超媒体(通常称为应用程序状态的引擎
(HATEOAS))是具体状态传输 (REST) 的显要限制有。有一样栽传统看超媒体项目(如链接或者表单)可用于证明客户端如何与同样组
HTTP 服务交互。这很快成一个诙谐的概念,在支付而演变的 API
设计时会见为此到其。这跟我们通常与 Web 交互的点子没有其他例外。我们日常记住网站主页的一个入口点或
URL,然后使链接浏览网站的逐条不同区域。我们尚利用表单,它附带预定义的操作还是 URL
以授网站实施某些操作所需要的数目。

开发人员倾向在服务中提供具有支持之方式的静态描述,从正规预约(如
SOAP 服务遭遇之 Web 服务描述语言 (WSDL))到无超媒体 Web API
中的简短文档都是这般。这样做的重大问题是静态 API
描述将客户端与服务器紧密关系。简而言之,它阻挡了可是演变性,因为 API
描述中之另变动都或半途而废所有现有客户端。

随即当可事先控制及询问客户端应用程序数目的商店被暂时不会见惹问题。但是,当潜在客户端数上指数级增长时(就比如时,数以千计的老三正在应用程序在差不多个装备及运行),这样做就是不得当了。简单地打
SOAP 迁移至 HTTP 服务并无克担保解决之问题。例如,如果以使算 URL
的客户端上提供部分学问,问题仍会存在,甚至没有 WSDL
之类的外显式约定。超媒体可以帮忙客户端屏蔽任何服务器移。

应用程序状态工作流也承诺在服务器端,它规定客户端接下来好举行啊。假定资源遭到的一个操作才对点名状态可用,该逻辑是不是应进驻留在肆意可能的
API 客户端?肯定特别。服务器应尽控制好本着资源执行什么操作。例如,如果撤销购买订单
(PO),就不应允许客户端应用程序提交该
PO,这象征在殡葬到客户端的应中许诺无法以提交该 PO
的链接或者表单。

超媒体应运而生

链接始终是 REST 体系布局的重要性组件。当然,链接以诸如浏览器的用户界面上下文中老广阔;例如,考虑下“参见详细信息”链接来获得目录中指定产品之详细信息。但是尚未用户界面或用户交互的处理器及电脑情形怎么处置也?我们认为,您也足以在这些状况中以超媒体项目。

采取这个新办法后,服务器不仅返回数据。它回到数据与超媒体项目。超媒体项目为客户端提供了同一种植方式,使它们好依据服务器应用程序工作流的状态来规定好于指定时间接触实施的操作集合。

当时是屡见不鲜区分常规 Web API 和支撑 REST 的 API
的一模一样远在,但是还存适用的其余限制,因此于大部分情形下讨论 API 是否支持
REST 可能无意义。我们设关爱之是 API 能否正确用 HTTP
作为应用程序协议并尽可能采取超媒体。通过启用超媒体,您可以创建而我发现的
API。这没有也免提供文档找借口,但是 API
在可更新性方面又灵活了。

得利用什么超媒体项目重点出于所选的传媒类型决定。我们当下用于构建 Web API 的大多媒体类型(如 JSON 或
XML)和 HTML 一样,不提供表示链接或者表单的搁概念。您得经过定义表示超媒体的办法来用这些媒体类型,但是及时要求客户端询问超媒体语义在其上是什么样定义之。相比之下,诸如 XHTML (application/xhtml+xml) 或 ATOM
(application/atom+xml)
的媒体类型已支持中的一部分超媒体项目(如链接或者表单)。


HTML 中,一个链接由三独片构成: 一个针对性 URL
的“href”属性,一个证实链接和时资源事关的“rel”属性与一个可选的“type”属性(用于指定要求的媒体类型)。例如,如果只要采取 XHTML
公开目录中的制品列表,资源负载可能类似于图 1 中所著之载荷。

图 1
使用 XHTML 公开产品列表

  1. <div id=”products”>
  2. <ul class=”all”>
  3. <li>
  4. <span class=”product-id”>1</span>
  5. <span class=”product-name”>Product 1</span>
  6. <span class=”product-price”>5.34</span>
  7. <a rel=”add-cart” href=”/cart” type=”application/xml”/>
  8. </li>
  9. <li>
  10. <span class=”product-id”>2</span>
  11. <span class=”product-name”>Product 2</span>
  12. <span class=”product-price”>10</span>
  13. <a rel=”add-cart” href=”/cart” type=”application/xml”/>
  14. </li>
  15. </ul>
  16. </div>

于此示例中,使用正式 HTML
元素表示产品目录,但是我动用了 XHTML,因为这样一来使用任意现有 XML
库分析会爱多。而且作为负载的同等片,包含了一个锚点 (a)
元素,表示用于将该项添加到目前用户购物车的链接。通过查该链接,客户端好由 rel
属性推断其用法(添加新项),并以 href 用于对该资源 (/cart)
执行一个操作。请留心,链接由服务器根据那工作工作流来变化,因此客户端不需对其它
URL 进行硬编码或推测任何规则。这也供了以运转时修改工作流的新机会而非影响现有客户端。如果目录中之任性产品缺货,服务器就需要忽略用于将该产品丰富到市物车的链接即可。从客户端角度看,该链接不可用,因此无法订购该产品。服务器端可能用了跟拖欠工作流有关的双重复杂的规则,但是客户端根本察觉不顶马上点,因为其唯一关注的事务是欠链接不有。由于超媒体和链接,客户端和服务器端的事体工作流已撤关联。

与此同时,可以以超媒体和链接改进 API
设计之只是演变性。随着服务器上作业工作流的不断完善,它好供用于新成效的另外链接。在我们的产品目录示例中,服务器可能含一个初链接用于将产品标志为收藏项,如下所示:

  1. <li>
  2. <span class=”product-id”>1</span>
  3. <span class=”product-name”>Product 1</span>
  4. <span class=”product-price”>5.34</span>
  5. <a rel=”add-cart” href=”/cart/1″ type=”application/xml”/>
  6. <a rel=”favorite” href=”/product_favorite/1″
  7. type=”application/xml”/>
  8. </li>

尽管现有客户端可能忽略该链接并无为之新职能的震慑,但是比新的客户端可立即开始以该功能。这样,考虑为公的 Web API 提供单个入口点或根 URL
也不怕不足吗惊异了,该入口点或根 URL 包含发现其余功能的链接。例如,您可享一个 URL“/shopping_cart”,它回到以下
HTML 表示形式:

  1. <div class=”root”>
  2. <a rel=”products” href=”/products”/>
  3. <a rel=”cart” href=”/cart”/>
  4. <a rel=”favorites” href=”/product_favorite”/>
  5. </div>

每当
OData 服务受到也供类似意义,该功能以根 URL
中公开一个劳动文档,该文档包含有支持的资源聚集与用于获取与该涉嫌的数码的链接。

链接是接二连三服务器和客户端的好方法,但是其有一个明了的题目。在有关产品目录的先示例中,HTML 中的一个链接只供
rel、href 和 type 属性,这暗含部分关于如何处理用 href 属性表示的该 URL
的牵动他知识。客户端应使用 HTTP POST 还是 HTTP GET?如果她应用
POST,应于请主体中寓什么数据?尽管具备知识或记录在某处,但是若客户端好实际发现该意义未另行好为?对于有所这些题材,使用 HTML
表单可以化解,它发为数不少意思。

操作中的表单

运用浏览器和 Web
交互时,通常使用表单表示操作。在产品目录示例中,按“添加至贾物车”链接暗示将 HTTP GET
发送到服务器,它用赶回一个但用以将成品丰富到买物车的 HTML
表单。该表单可以分包一个带 URL 的“action”属性、一个象征 HTTP
方法的“method”属性与一部分恐要求用户输入的输入字段,还含有可读之累操作的征。

君得于电脑及计算机情形被做一样的事体。如果不思量经过人工和表单交互,您可能要周转 JavaScript
或 C# 的应用程序。在产品目录中,用于访问第一只活之“add-cart”链接的 HTTP
GET 将追寻用 XHTML 表示的以下表单:

  1. <form action=”/cart” method=”POST”>
  2. <input type=”hidden” id=”product-id”>1</input>
  3. <input type=”hidden” id=”product-price”>5.34</input>
  4. <input type=”hidden” id=”product-quantity” class=”required”>1</input>
  5. <input type=”hidden” id=”___forgeryToken”>XXXXXXXX</input>
  6. </form>

客户端应用程序现在早已和涉及以成品丰富到买入物车的一些详细信息取消关联。它独自需要运用
HTTP POST 将这表单提交到 action 属性中指定的 URL。服务器还得于表单中富含其他消息,例如,包含一个伪造标记为避免超过站点请求伪造
(CSRF) 攻击或者对先行为服务器填充的数目进行签字。

以此模型允许擅自 Web API
通过根据不同因素(如用户权限或客户端要使用的本)提供新表单来随便演变。

用以 XML 和 JSON 的超媒体?

若是本人在前文中所陈述,XML (application/­xml) 和 JSON
(application/json)
的通用媒体类型没有针对超媒体链接或者表单的放支持。尽管可以使用域特定的定义(如“application/vnd-shoppingcart+xml”)扩展这些媒体类型,但是及时要求新客户端询问在初路受到定义的具有语义(并还可能衍生媒体类型),因此一般不这样做。

恰巧因为这么,有人提出了利用链接语义扩展 XML 和 JSON
的新媒体类型建议,它称作也超文本应用程序语言 (HAL)。该草案在
stateless.co/hal_specification.html 上发布,它大概定义一个利用 XML 和
JSON 表示过链接和停放资源(数据)的正儿八经方法。HAL
媒体类型定义包含一组属性、一组链接和同一组嵌入资源的资源,如图 2
中所示。

XML 1.png) 希冀 2 HAL
媒体类型


3
显示一个演示,它说明产品目录在又用 XML 和 JSON 表示形式之 HAL
中是呀则。图 4 是示例资源的 JSON 表示形式。

图 3
HAL 中的产品目录

  1. <resource href=”/products”>
  2. <link rel=”next” href=”/products?page=2″ />
  3. <link rel=”find” href=”/products{?id}” templated=”true” />
  4. <resource rel=”product” href=”/products/1″>
  5. <link rel=”add-cart” href=”/cart/” />
  6. <name>Product 1</name>
  7. <price>5.34</price>
  8. </resource>
  9. <resource rel=”product” href=”/products/2″>
  10. <link rel=”add-cart” href=”/cart/” />
  11. <name>Product 2</name>
  12. <price>10</price>
  13. </resource>
  14. </resource>

祈求 4
示例资源的 JSON 表示形式

  1. {
  2. “_links”: {
  3. “self”: { “href”:
    “/products” },
  4. “next”: { “href”:
    “/products?page=2” },
  5. “find”: { “href”:
    “/products{?id}”, “templated”: true }
  6. },
  7. “_embedded”: {
  8. “products”: [{
  9. “_links”: {
  10. “self”: { “href”:
    “/products/1” },
  11. “add-cart”: { “href”:
    “/cart/” },
  12. },
  13. “name”: “Product 1”,
  14. “price”: 5.34,
  15. },{
  16. “_links”: {
  17. “self”: { “href”:
    “/products/2” },
  18. “add-cart”: { “href”:
    “/cart/” }
  19. },
  20. “name”: “Product 2”,
  21. “price”: 10
  22. }]
  23. }
  24. }

于 ASP.NET Web API 中支持超媒体

每当前面文中,我们谈论了当计划 Web API
时只要按照的有超媒体原理。现在我们来打听一下怎么样以采取 ASP.NET Web API
的产条件遭受其实履行这些规律,并以是框架提供的有着可扩展性和效用。

以基础级别,ASP.NET Web API
支持格式化程序的概念。格式化程序实现形式知道怎么处理特定媒体类型,以及哪拿其序列化或反序列化为实际的
.NET 类型。过去在 ASP.NET MVC
中针对新媒体类型的支撑特别少。只有 HTML 和
JSON 被视为有效成员并以周堆栈中获得完全支持。此外,没有用来支持内容商的同样模型。您可由此提供于定义 ActionResult
实现来支撑响应消息的异媒体类型格式,但是她不懂得什么引入新媒体类型来反而序列化请求消息。利用具有新的模子绑定程序要值提供程序的型绑定基础结构通常可以解决这个题材。幸运的是,这种不一致性在 ASP.NET Web API
中一度透过引入格式化程序获取解决。

每个格式化程序由基类
System.Net.Http.Formatting.MediaTypeFormatter 派生并重新写方法
CanReadType/ReadFromStreamAsync 以支持反序列化,重写方法
CanWriteType/WriteToStreamAsync 以支撑以 .NET
类型序列化为指定的媒体类型格式。


5
显示 MediaTypeFormatter 类的定义。


5 MediaTypeFormatter 类

  1. public abstract class MediaTypeFormatter
  2. {
  3. public Collection<Encoding>
    SupportedEncodings { get; }
  4. public
    Collection<MediaTypeHeaderValue> SupportedMediaTypes { get; }
  5. public abstract bool CanReadType(Type type);
  6. public abstract bool CanWriteType(Type type);
  7. public virtual Task<object> ReadFromStreamAsync(Type type,
  8. Stream readStream,
  9. HttpContent content, IFormatterLogger formatterLogger);
  10. public virtual Task WriteToStreamAsync(Type
    type, object value,
  11. Stream writeStream, HttpContent content,
  12. TransportContext transportContext);
  13. }

格式化程序在 ASP.NET Web API
中对此支撑内容商于在至关重要作用,因为框架现在可依据当呼吁消息之“Accept”和“Content-Type”标头中收取的价值选择对的格式化程序。

ReadFromStreamAsync 和 WriteToStreamAsync
方法依赖任务并行库 (TPL) 来施行异步操作,因此它返回 Task
实例。如果您而显式使格式化程序实现同步工作,基类
BufferedMediaTypeFormatter 将当里面也汝执行这操作。此基类提供你得在实现中另行写的有限只点子 SaveToStream 和
ReadFromStream,它们是 SaveToStreamAsync 和 ReadFromStreamAsync
的联名版本。

支出用于 HAL 的 MediaTypeFormatter

HAL
以一定语义来代表资源以及链接,因此你不能够只是利用 Web API
实现着之其它模型。为是,我们采取一个用于表示资源的基类和另外一个用来表示资源集的基类来使格式化程序的贯彻重新简单:

  1. public abstract class LinkedResource
  2. {
  3. public List<Link> Links {
    get; set; }
  4. public string HRef { get; set; }
  5. }
  6. public abstract class LinkedResourceCollection<T> :
    LinkedResource,
  7. ICollection<T> where T : LinkedResource
  8. {
  9. // Rest of the collection
    implementation
  10. }

Web
API 控制器将使的实际模型类可打立点儿独基类派生。例如,一个成品还是制品聚集好随以下方式实现:

  1. public class Product : LinkedResource
  2. {
  3. public int Id { get; set; }
  4. public string Name { get; set; }
  5. public decimal UnitPrice { get; set; }
  6. }
  7. public class Products :
    LinkedResourceCollection<Product>
  8. {
  9. }

当今,有了定义 HAL
模型的业内措施,因此可实现格式化程序了。生成新的格式化程序实现的顶简便方法是打
MediaTypeFormatter 基类或 BufferedMediaTypeFormatter
基类派生。图 6 中之示范使用了亚单基类。


6 BufferedMediaTypeFormatter 基类

  1. public class HalXmlMediaTypeFormatter :
    BufferedMediaTypeFormatter
  2. {
  3. public HalXmlMediaTypeFormatter()
  4. : base()
  5. {
  6. this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(
  7. “application/hal+xml”));
  8. }
  9. public override bool CanReadType(Type type)
  10. {
  11. return type.BaseType == typeof(LinkedResource) ||
  12. type.BaseType.GetGenericTypeDefinition() ==
  13. typeof(LinkedResourceCollection<>);
  14. }
  15. public override bool CanWriteType(Type type)
  16. {
  17. return type.BaseType == typeof(LinkedResource) ||
  18. type.BaseType.GetGenericTypeDefinition() ==
  19. typeof(LinkedResourceCollection<>);
  20. }
  21. }

欠代码首先在构造函数中定义支持之这实现之传媒类型(“application/hal+xml”),然后还写
CanReadType 和 CanWriteType 方法以指定支持之 .NET 类型,这些项目必须由
Linked­Resource 或 LinkedResourceCollection 派生。因为已经于构造函数中定义,此实现就支持 HAL 的 XML
变体。还可以兑现任何一个格式化程序来支撑 JSON
变体(可选)。

实则工作在 WriteToStream 和 ReadFromStream
方法被就(如图 7 中所示),这些方法以分头使用 XmlWriter 和
XmlReader 来用目标写入流或打流中读取对象。


7 WriteToStream 和 ReadFromStream 方法

  1. public override void WriteToStream(Type type, object value,
  2. System.IO.Stream writeStream, System.Net.Http.HttpContent content)
  3. {
  4. var encoding = base.SelectCharacterEncoding(content.Headers);
  5. var settings = new
    XmlWriterSettings();
  6. settings.Encoding = encoding;
  7. var writer = XmlWriter.Create(writeStream, settings);
  8. var resource = (LinkedResource)value;
  9. if (resource is IEnumerable)
  10. {
  11. writer.WriteStartElement(“resource”);
  12. writer.WriteAttributeString(“href”,
    resource.HRef);
  13. foreach (LinkedResource
    innerResource in
    (IEnumerable)resource)
  14. {
  15. // Serializes the resource state and links
    recursively
  16. SerializeInnerResource(writer, innerResource);
  17. }
  18. writer.WriteEndElement();
  19. }
  20. else
  21. {
  22. // Serializes a single linked resource
  23. SerializeInnerResource(writer, resource);
  24. }
  25. writer.Flush();
  26. writer.Close();
  27. }
  28. public override object ReadFromStream(Type type,
  29. System.IO.Stream readStream, System.Net.Http.HttpContent content,
  30. IFormatterLogger formatterLogger)
  31. {
  32. if (type != typeof(LinkedResource))
  33. throw new ArgumentException(
  34. “Only the LinkedResource type is
    supported”, “type”);
  35. var value = (LinkedResource)Activator.CreateInstance(type);
  36. var reader = XmlReader.Create(readStream);
  37. if (value is IEnumerable)
  38. {
  39. var collection = (ILinkedResourceCollection)value;
  40. reader.ReadStartElement(“resource”);
  41. value.HRef = reader.GetAttribute(“href”);
  42. var innerType = type.BaseType.GetGenericArguments().First();
  43. while (reader.Read() &&
    reader.LocalName == “resource”)
  44. {
  45. // Deserializes a linked resource
    recursively
  46. var innerResource = DeserializeInnerResource(reader, innerType);
  47. collection.Add(innerResource);
  48. }
  49. }
  50. else
  51. {
  52. // Deserializes a linked resource
    recursively
  53. value = DeserializeInnerResource(reader, type);
  54. }
  55. reader.Close();
  56. return value;
  57. }

末一步是将格式化程序实现作为 Web API
宿主的同等局部部署。此步骤几乎可据此和于 ASP.NET 或 ASP.NET Web API
自托管中平等的法子来贯彻,只是所急需的 HttpConfiguration
实现不同。尽管自托管使用 HttpSelfHostConfiguration 实例,ASP.NET
通常采取于 System.Web.Http.GlobalConfiguration.Configuration
中全局可用的 HttpConfiguration 实例。HttpConfiguration 类提供一个 Formatters
集合,您可以将它注入自己的格式化程序实现。以下是何许对 ASP.NET 执行是操作:

  1. protected void Application_Start()
  2. {
  3. Register(GlobalConfiguration.Configuration);
  4. }
  5. public static void Register(HttpConfiguration config)
  6. {
  7. config.Formatters.Add(new
    HalXmlMediaTypeFormatter());
  8. }


ASP.NET Web API 管道遭配置格式化程序后,任何控制器采用 HAL
都好略地回到一个型类,该型类从格式化程序要序列化的
LinkedResource 派生。对于产品目录实例,产品跟代表目录的制品聚集好分别由
LinkedResource 和 LinkedResourceCollection 派生:

  1. public class Product : LinkedResource
  2. {
  3. public int Id { get; set; }
  4. public string Name { get; set; }
  5. public decimal UnitPrice { get; set; }
  6. }
  7. public class Products :
    LinkedResourceCollection<Product>
  8. {
  9. }

用于拍卖产品目录资源的所有请求的控制器
ProductCatalogController 现在好为 Get 方法返回 Product 和 Products
的实例(如图 8 中所示)。


8 ProductCatalogController 类

  1. public class ProductCatalogController :
    ApiController
  2. {
  3. public static Products Products = new Products
  4. {
  5. new Product
  6. {
  7. Id = 1,
  8. Name = “Product 1”,
  9. UnitPrice = 5.34M,
  10. Links = new List<Link>
  11. {
  12. new Link { Rel = “add-cart”, HRef = “/api/cart” },
  13. new Link { Rel = “self”, HRef = “/api/products/1” }
  14. }
  15. },
  16. new Product
  17. {
  18. Id = 2,
  19. Name = “Product 2”,
  20. UnitPrice = 10,
  21. Links = new List<Link>
  22. {
  23. new Link { Rel = “add-cart”, HRef = “/cart” },
  24. new Link { Rel = “self”, HRef = “/api/products/2” }
  25. }
  26. }
  27. };
  28. public Products Get()
  29. {
  30. return Products;
  31. }
  32. }

此示例祭 HAL 格式,但是若还可以运用类似措施来构建以
Razor 的格式化程序和拿模型序列化为 XHTML 的沙盘。您当
RestBugs 中可以找到用于 Razor 的 MediaTypeFormatter
的现实性实现,该示例应用程序由 Howard Dierking 创建,演示如何采取 ASP.NET
Web API 来创造超媒体 Web API,网址为
github.com/howarddierking/RestBugs。

格式化程序一旦您得轻松使用新媒体类型扩展 Web API。   

当 Web API 控制器中提供再好的链接支持

先的 ProductCatalog­Controller
示例肯定起不妥之处。其中的保有链接都硬编码了,如果路由经常转移,会教人头疼不已。幸好框架提供了名吧 System.Web.Http.Routing.UrlHelper
的援助器类来机关从路由表推断链接。通过 Url
属性在 ApiController
基类中提供此类的实例,因此可在外控制器方法被轻松利用她。UrlHelper
类定义类似于:

  1. public class UrlHelper
  2. {
  3. public string Link(string routeName,
  4. IDictionary<string, object> routeValues);
  5. public string Link(string routeName, object routeValues);
  6. public string Route(string routeName,
  7. IDictionary<string, object> routeValues);
  8. public string Route(string routeName, object routeValues);
  9. }

Route
方法返回指定路由于的对立 URL(例如 /products/1),Link 方法返回绝对
URL(可以当范中以该 URL 来避免硬编码)。Link
方法接收两单变量: 路由名称与苟做 URL 的值。


9
显示对以前的产品目录示例,如何当 Get 方法被利用 UrlHelper
类。

希冀
9 如何在 Get 方法吃运用 UrlHelper 类

  1. public Products Get()
  2. {
  3. var products = GetProducts();
  4. foreach (var product in products)
  5. {
  6. var selfLink = new Link
  7. {
  8. Rel = “self”,
  9. HRef = Url.Route(“API Default”,
  10. new
  11. {
  12. controller = “ProductCatalog”,
  13. id = product.Id
  14. })
  15. };
  16. product.Links.Add(selfLink);
  17. if(product.IsAvailable)
  18. {
  19. var addCart = new Link
  20. {
  21. Rel = “add-cart”,
  22. HRef = Url.Route(“API Default”,
  23. new
  24. {
  25. controller = “Cart”
  26. })
  27. };
  28. product.Links.Add(addCart);
  29. }
  30. }
  31. return Products;
  32. }

既以控制器名称 ProductCatalog 和成品 ID
从默认路出于死成了成品的链接“self”。还于默认路是因为中生成了用于将产品增长到打物车的链接,只是利用的控制器名称为
Cart。如图 9
中所展示,用于将产品增长到买物车的链接根据产品可用性 (product.IsAvailable)
与应关联。向客户端提供链接的逻辑主要依赖让通常以控制器中履行之事情规则。

总结

超媒体的力量很有力,允许客户端和服务器独立演变。通过在不同等级采用服务器提供的链接或者外超媒体项目(如表单),客户端好成功和驱动交互的服务器业务工作流取消关联。

 

Pablo Cibraro 大凡国际直达公认的大家,在利用 Microsoft
技术计划和促成大型分布式系统方面具备超越 12 年的长经历。 他是并肩系统
MVP。 最近 9 年面临,Cibraro 帮助多 Microsoft
团队开发了部分器与框架,以便为采取 Web 服务、Windows Communication
Foundation、ASP.NET 和 Windows Azure 构建面向服务的应用程序。
他的博客地址是
weblogs.asp.net/cibrax,您可在
Twitter twitter.com/cibrax
上关注外。

原文:
http://msdn.microsoft.com/zh-cn/magazine/jj883957.aspx 

相关文章

网站地图xml地图