Web API 和 Windows Azure。第一部分。历史,关于 REST,示例。





5.00/5 (2投票s)
本系列将介绍 Web API 及其在 Windows Azure 中的使用
在这一部分
- 历史
- 既然有了 WCF,Web API 的用途是什么?
- REST 概念和 RMM 模型
- 第一个 ASP.NET MVC + Web API + jQuery + AJAX 应用示例
- 在 Web API 中使用 OData
所以,为了理解这一切的用途,我们需要明确主题及其目的。但让我们从一些历史开始,以(从我的角度)澄清 Web API 引入的必要性。
历史
不深入过去细节,最好提到微软在开发面向服务的系统方面走过很长的路,主要支柱是:ASMX、.NET Remoting、WCF。使用基于 ASMX 的 Web 服务(大约在 2002 年),开发人员可以相当容易地创建实现了各种 SOAP 版本的 Web 服务,但它只能通过 HTTP 访问。同时,Remoting 技术允许服务开发不仅使用 HTTP。2006 年,微软发布了 .NET 3.0 和 Windows Communication Foundation (WCF),它涵盖了 ASMX 和 .NET Remoting 的功能,并提供了更多有趣的选项。例如,使用 WCF 可以轻松地为 TCP 开发 Web 服务,并带有基于令牌的身份验证,以及在“手动”开发的服务中运行。我个人在 2009 年接触 WCF,当时我为几十台机器开发了日志收集的程序基础设施——主节点机器上运行着 WCF 服务,而其他机器则每 N 分钟执行一次 Powershell 脚本,调用 WCF 服务并发送数据。我不敢说第一次操作很容易,但我欣赏这种方法的优雅。2007 年,ASP.NET MVC 框架发布了 CTP 版本。即使在那时,这个想法已经浮现:互联网使用起来非常方便——网页信息交换的过程主要使用文本消息而不是二进制数据,使用 HTTP,客户端部分处理各种任务。这导致了一个想法,即开发一种仅支持 XML 和 JSON 序列化的东西。如果我想创建最简单的 RESTful 服务,我特别会使用部分笨重的 WCF,它最初并不支持 REST,这有什么用呢?于是 ASP.NET MVC 出现了,它内置了路由机制。
MVC 路由
与 WCF 不同,在 WCF 中,服务等同于物理文件路径,MVC 路由机制的工作方式不同:它将地址与控制器方法的地址相关联。例如,如果我们使用 WCF,那么对服务的调用将是
如果我们使用 ASP.NET MVC
http://server/MyService/Get/123如您所见,这种寻址方式可以封装内部行为。MVC 路由还允许您将请求从上述地址转移到任意控制器方法 。所有这些都大大简化了后续应用程序的支持:我们可以以方便的方式更改内部行为,例如,只需修改路由配置即可快速替换旧实现。通过这种路由机制,Web API 的创建(实际上是 ASP.NET 的一个附加层)使得开发 HTTP REST 服务成为可能,这些服务与 ASP.NET MVC 在同一个项目中工作,并且可以完全集成到 MVC 管道中(逻辑上,而不是实现上)。但在您开始使用 Web API 编写 REST 服务之前,我们需要澄清 REST 服务是什么意思——下面继续。
关于 REST 的一点说明
有人说,使链接漂亮就是使 API 100% 符合 REST。确实,这不是真的,REST(第一次出现在一位聪明人的一篇论文中)不像看起来那么简单。REST 服务的构建规则涉及到一些限制,这些限制也定义了架构,所以您必须考虑到这一点,并同意或不同意它。
为了理解 REST 的核心,我们将使用 Leonardo Richardson 于 2008 年引入的 RMM(REST 成熟度模型)。那些熟悉 CMMI(能力成熟度模型集成)的人会立即注意到词语的相似性——这并非偶然。与 CMMI 类似,该模型描述了遵守 一套规则和方法论的几个级别。RMM 包含 4 个级别——从 0 到 3。在该模型中,一切都从 0 级的最简单事物开始,其中 API 对应于 RPC 风格,最后则符合所有基本范例——REST。当然,如果您使用此系统来描述自己的模型并在低于 3 的某个级别上卡住——您的服务就不是 REST。让我们看一张不太艺术的 RMM 图,然后根据 RMM 研究一个留言簿服务的示例。

0 级
根据 0 级,我们只会有一个 WCF 服务 GuestService,它有一个方法 CreatePost(),该方法接收记录的标题、用户名和电子邮件等参数。该方法将返回记录的编号。对于管理员,将有更多的方法——DeletePost() 和 UpdatePost()。每个方法都会接收一个消息并返回一个答案。当然,还有 GetAllPosts()。一个简单的系统。
所以,我们 0 级 RMM 的服务看起来是这样的
方法 |
URL |
HTTP 动词 |
Visibility |
CreatePost |
/api/GuestService.svc |
POST |
例如,WSDL |
DeletePost |
/api/GuestService.svc |
POST |
例如,WSDL |
UpdatePost |
/api/GuestService.svc |
POST |
例如,WSDL |
GetAllPosts |
/api/GuestService.svc |
POST |
例如,WSDL |
根据表格,我们将有一个 API,所有操作都有自己的链接(URL)。但是,链接看起来完全不明确且与语义上下文无关:无论我们是删除记录 #1 还是更新记录 #100——URL 都将保持不变。这样的系统对应于 RMM 的 0 级及其主要特征:一个 URL = 一个 HTTP 动词。HTTP 动词的结果也一样:所有东西都使用 POST,而且您必须编写自己的方法。RESTful HTTP 的规则之一是不需要创建新方法:只需遵循包含所有必要 HTTP 动词的列表。稍后我们将讨论它们。
当然,值得注意的是,客户端(在这种情况下是服务)必须知道如何调用所需的操作,也就是说,是否存在逻辑连接,例如契约(这里是 WSDL)。但我们真的需要额外的逻辑连接吗?即使是 REST 也要求我们只知道基本 URL,而所有其他操作都应该可以通过所谓的超媒体元素(链接、表单等)访问。服务器应控制整个过程并指定链接和表单的外观,而不与客户端共享此信息,以便在系统发生任何更改时做出适当反应。这些都是 HATEOAS 原则,稍后将讨论。那么,让我们进入 1 级。
1 级
所以,我们决定让我们的服务成为 REST 服务。需要采取哪些步骤?首先,我们的服务应该符合 1 级,也就是说,REST 服务应该是面向资源的:使用 HTTP 动词访问资源,而不是请求-响应模式和类似 RPC 的方法——就是这样。GET、DELETE、POST,有时是 PUT,偶尔是 HEAD。这个原则是 REST 服务的基础:如果您的服务提供大量各种方法,它就不是 REST 服务(根据 RMM)。
因此,遵循 1 级规则“任意 URL = 一个 HTTP 动词”,让我们为 API 创建一个新表。
方法 |
URL |
HTTP 动词 |
Visibility |
CreatePost |
/api/posts |
POST |
例如,WSDL |
DeletePost |
/api/posts/1 |
POST |
例如,WSDL |
UpdatePost |
/api/posts/1 |
POST |
例如,WSDL |
GetAllPosts |
/api/posts |
POST |
例如,WSDL |
但这里有一个 RESTful 问题:客户端仍然不明白链接是如何区分的,没有服务契约:/api/posts 和 /api/posts 以及,例如,WSDL。客户端应该有一个契约。内聚级别保持不变,此外,仍然遵守一个 HTTP 动词。至少我们的服务达到了 RMM 的 1 级。让我们尝试进入 2 级。
2 级
2 级规则是“任意 URL = 任意 HTTP 动词”。这时我们必须重新考虑我们的服务作为面向资源的。客户端发送什么?如何处理?发送到哪里?
事实上,CreatePost 和 GetAllPosts 等 HTTP 动词并不存在。但有 POST、GET、PUT 和 DELETE,所以我们可以利用它们并进入 2 级。请注意,HTTP 动词具有一些特性,例如,PUT 和 DELETE 是幂等的。也就是说,无论何时调用它们,它们都会返回与 HTTP 响应相同的结果:例如,无论何时调用 PUT,它都会更改相同的实体。POST 不具有幂等性是出于一个已知的原因:创建一个新实体。GET 不具有幂等性,但它是安全的——系统中没有发生更改,调用 GET 时必须如此。这一点非常重要,我见过一个系统,GET 方法在服务器端显示数据源中实体状态的逻辑更改——这是不对的。
那么,让我们看看我们 2 级下的服务。
方法 |
URL |
HTTP 动词 |
Visibility |
CreatePost |
/api/posts |
POST |
例如,WSDL |
DeletePost |
/api/posts/1 |
删除 |
例如,WSDL |
UpdatePost |
/api/posts/1 |
PUT |
例如,WSDL |
GetPost |
/api/posts |
GET |
例如,WSDL |
看起来好多了。现在我们有一个与 HTTP 动词和各种 URL(主要是)对应的服务。但我们仍然与客户端紧密耦合。要解决这个问题,我们进入 3 级。
3 级
3 级是 HATEOAS。这个缩写的含义是让客户端只知道初始站点地址。其余信息将通过链接、按钮、其他语义元素提供给客户端。客户端应该能够访问资源及其状态,而无需事先了解如何操作。
因此,3 级对应的 API 在表中提供。请注意,即使服务符合所有 RMM 特征,也可能存在一些细微差别导致一切崩溃。
方法 |
URL |
HTTP 动词 |
Visibility |
CreatePost |
/api/posts |
POST |
HTTP POST |
DeletePost |
/api/posts/1 |
删除 |
HTTP DELETE |
UpdatePost |
/api/posts/1 |
PUT |
HTTP PUT |
GetPost |
/api/posts |
GET |
HTTP GET |
关于 REST 的最后几句话
最后几句话关于 REST:记住标准 HTTP 错误代码很重要,这样您的服务才能符合 REST。服务应返回已知的错误代码。这些术语不一定要遵守,但它们的创建和使用得到了最聪明的人的认可,并且有助于支持服务的人理解代码作者在创建服务时并非无能。
状态码 |
含义 |
200 |
OK。 |
201 |
实体已创建,响应可以包含指向新创建实体的链接 |
202 |
与 200 相同,但用于异步操作。 |
301 |
资源已移动。响应可以包含指向新资源的链接。 |
400 |
请求错误(客户端)。 |
401 |
未授权。 |
403 |
访问被拒绝——客户端已通过身份验证但未授权。 |
404 |
未找到资源,或者客户端没有访问权限,并且不应该知道原因。 |
409 |
服务器错误。 |
500 |
服务器错误。 |
Web API
回想一下,Web API 出现在 ASP.NET MVC 4 中,并引起了很多讨论:又一项用于创建网站的技术,又一个 REST——有什么用?这是一个好问题——当有 WCF 等各种技术时。但这些都是“全局”的东西,当涉及到一些大型应用程序实现时,您将不得不拖入一些“以防万一”可能有用但不太确定的工具。Web API 是一个可以通过 HTTP 轻松、快速、漂亮地实现 RESTful 服务的工具。
让我们创建一个 ASP.NET MVC 4 应用程序,并选择相应的 Web API 模板。
与任何控制器一样,脚手架会生成代码:在 Web API 控制器的情况下,它是 HTTP 动词方法的存根:GET、POST、PUT 和 DELETE。
HTTP 动词 |
控制器方法 |
描述 |
Get() |
GET |
方法返回任何 Ienumerable 相关类型的数组。 |
Get(string) |
GET |
方法根据方法参数返回单个实体。 |
Post(string) |
POST |
方法添加一个新实体。 |
Put(string,string) |
PUT |
方法更新现有实体。Put 和 Post 之间的区别在于 Post 始终创建一个新实体。 |
Delete(string) |
删除 |
方法从系统中删除一个实体。 |
让我们向测试数据库添加一个上下文。在这个系列文章中,我将使用我一直在 http://atraining.ru 课程中使用的测试数据库。它由两个非常简单且不相关的表组成:CourseSet 和 Student。
让我们对 WebAPI 控制器进行一些更改并相应地定义其方法。由于代码足够简单,我没有添加注释。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web.Http;
using WebApiJsonAjax.Models;
namespace WebApiJsonAjax.Controllers
{
public class ValuesController : ApiController
{
private atrainingdatabaseEntities
_ctx = new atrainingdatabaseEntities();
// GET api/values
public List<Models.Student>
Get()
{
var students = from e in _ctx.Student select e;
return students.ToList();
}
// GET api/values/5
public string Get(int id)
{
return
"value" + id;
}
public Student Post(Student student)
{
return
student;
}
// PUT api/values/5
public void Put(int id,
[FromBody]string value)
{
}
// DELETE api/values/5
public void Delete(int id)
{
var student
= _ctx.Student.Where(s => s.id == id).FirstOrDefault();
_ctx.Student.Remove(student);
_ctx.SaveChanges();
}
}
}
我们保存控制器并进行测试:按 F5 并访问 https://:[port]/api/values。请注意,当有人使用 GET 动词调用此链接时,他们会收到一个 JSON 结果。WebAPI 可以处理并返回 JSON 和 XML 值:这取决于客户端的配置。让我们看看 Chrome 和 IE 在这种情况下返回什么(截图分别放置),以便了解另一个 Web API 功能——内容协商。
Student 类型对象的整个 JSON 序列化过程由框架执行(这非常方便),与 ASP.NET MVC 相比:如果我们想从控制器返回 JSON,我们必须显式使用 return Json。让我们讨论一下为什么两个浏览器返回的服务结果格式不同。
内容协商
HTTP 标准中的内容协商是客户端和服务器之间通信细节协商的过程。当客户端调用服务器时,它会发送一个带有 Accept 指令的查询,指明期望得到什么响应。让我们使用“curl”实用程序执行 GET,以跟踪过程。提供了 Fiddler 截图。此外,我们还将使用浏览器内置应用程序查看 Chrome 和 IE 的查询。

Fiddler 界面与 curl 并行工作。

Accept 值是客户端希望从服务器接收的内容。从截图中可以看出,Chrome 希望“看到”text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8(q 是格式的“权重”:权重越大,客户端越希望以特定格式看到结果,并告诉服务器)。而 IE 希望看到 text/html, application/xhtml+xml, */*。请注意,Web API 无法识别 IE 想要看到的内容,并默认发送 JSON 结果,而 JSON 不支持 application/xhtml+xml,但支持 */*,这意味着“我将接收服务器返回的任何类型的响应”。Chrome 以期望的 XML 格式获得其所需内容。
让我们看看 curl 和 Fiddler 是如何做到的。
JSON 响应对于不关心结果格式的客户端。
让我们明确指出我们想看到什么。
非常棒:结果格式取决于客户端的要求。
正是这个过程被称为内容协商,它是非常重要的客户端-服务器通信过程。请注意格式分辨率是多么简单和优雅。结果格式的指示导致了下面的更复杂代码。
public
HttpResponseMessage Get()
{
var students = (from e in _ctx.Student select e);
var resp = new HttpResponseMessage(HttpStatusCode.OK);
resp.Content = new
ObjectContent<IEnumerable<Student>>(students, new
JsonMediaTypeFormatter());
resp.Headers.ConnectionClose = true;
resp.Headers.CacheControl = new CacheControlHeaderValue();
resp.Headers.CacheControl.Public = true;
return resp;
}
如果您想返回独立于客户端偏好的格式结果,您必须访问将为结果创建的消息,并使用特殊机制——Formatter——来定义其属性。不用说,您可以创建自己的具有更复杂逻辑结构的转换器。所以,在上面的代码中
- 我们接收 Student 集合
- 创建一个带有 201 OK 状态的 HttpResponseMessage 类实例。此消息将返回给客户端。
- 消息包含我们的数据模型,它被转换为 JSON 格式。
- 消息返回给客户端。
让我们在 curl 中重复我们的查询,告诉我们只想看到 application/xml。
尽管有 Accept: application/xml,但结果仍然以 JSON 格式返回。
Web API 中的内容协商之美。我们继续。
使用 jQuery 从 MVC 视图中使用 Web API
所以,我们的 REST 服务几乎完成了。Web API 自然不支持任何视图生成机制。让我们将服务连接到 MVC 视图。
我们将从 HomeController 的 Index 视图中使用 Web API。jQuery 将从服务中获取数据,我们将创建一个新表。
整个页面代码在此提供。
<header>
<div class="content-wrapper">
<div
class="float-left">
<p
class="site-title">
<a href="~/">ASP.NET Web API</a></p>
</div>
</div>
</header>
<div id="body">
<section class="featured">
<div
class="content-wrapper">
<table
id="students"></table>
<script>
$(function () {
var $students = $("#students");
$.ajax({
url: "api/values",
contentType: "json",
success: function(data) {
$.each(data, function(index, item) {
var $row =
$("#templates").find(".row-template").clone();
$row.find(".Name").html(item.name);
$row.find(".delete").click(function() {
$.ajax({ url: "api/values/" + item.id,
type: "DELETE",
success: function() {
$row.remove();
}
});
});
$students.append($row);
});
}
});
})
</script>
</div>
</section>
<section class="content-wrapper main-content
clear-fix">
<div id="templates"
style="display:none">
<table>
<tr class="row-template">
<td class="Name"></td>
<td><input type="button" value ="Del"
class="delete"/></td>
</tr>
</table>
</div>
</section>
</div>
代码相当简单:我们从服务器获取数据并创建一个动态表。
删除一条记录意味着点击附加的按钮。
让我们添加剩余的功能:PUT 和 POST,这将需要大量的代码。我提供了整个视图和控制器列表。
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Web.Http; using WebApiJsonAjax.Models; namespace WebApiJsonAjax.Controllers { public class ValuesController : ApiController { private atrainingdatabaseEntities _ctx = new atrainingdatabaseEntities(); public HttpResponseMessage Get() { var students = (from e in _ctx.Student select e); var resp = new HttpResponseMessage(HttpStatusCode.OK); resp.Content = new ObjectContent<IEnumerable<Student>>(students, new JsonMediaTypeFormatter()); resp.Headers.ConnectionClose = true; resp.Headers.CacheControl = new CacheControlHeaderValue(); resp.Headers.CacheControl.Public = true; return resp; } // GET api/values/5 public string Get(int id) { return "value" + id; } public HttpResponseMessage Post([FromBody]Student student) { try { _ctx.Student.Add(student); _ctx.SaveChanges(); } catch (Exception e) { return Query.CreateErrorResponse(HttpStatusCode.InternalServerError, e.Message); { try { _ctx.Student.Add(student); _ctx.SaveChanges(); } catch (Exception e) { return Query.CreateErrorResponse(HttpStatusCode.InternalServerError, e.Message); }; return Query.CreateResponse(HttpStatusCode.OK); } // PUT api/values/5 public HttpResponseMessage Put(int id, Student student) { try { var studentToChange = _ctx.Student.FirstOrDefault(s => s.id == id); studentToChange.name = student.name; _ctx.SaveChanges(); } catch (Exception e) { return Query.CreateErrorResponse(HttpStatusCode.InternalServerError, e.Message); }; return Query.CreateResponse(HttpStatusCode.OK); } // DELETE api/values/5 public void Delete(int id) { var student = _ctx.Student.Where(s => s.id == id).FirstOrDefault(); _ctx.Student.Remove(student); _ctx.SaveChanges(); } } }
PUT 和 POST 方法实现在控制器中。两个方法中的操作都包含在 try/catch 块中,如果出现问题,将创建并发送一条消息。请注意,有一个非常有用的属性:您可以将控制器方法参数标记为 [FromBody] 或 [FromUrl],它们分别表示从查询正文或 URL 中获取参数。
视图代码将在下面提供。添加了记录创建和编辑事件的处理程序。请注意,JSON.stringify 将所有内容转换为 JSON。
<header>
<div class="content-wrapper">
<div
class="float-left">
<p
class="site-title">
<a href="~/">ASP.NET Web API</a></p>
</div>
</div>
</header>
<div id="body">
<section class="featured">
<div
class="content-wrapper">
<table
id="students"></table>
<script>
$(function() {
var $students = $("#students");
$.ajax({
url: "api/values",
contentType: "json",
success: function(data) {
$.each(data, function(index, item) {
var $row = $("#templates").find(".row-template").clone();
$row.find(".Name").html("<input type=’text’ class=’studentname’
id=’" + item.id + "’ value=’" + item.name +
"’></input>");
$row.find(".delete").click(function() {
$.ajax({
url: "api/values/" + item.id,
type: "DELETE",
success: function() {
$row.remove();
}
});
});
$row.find(".change").click(function() {
var student = {
id: item.id,
name: $row.find(".studentname").attr("value")
};
var student = {
id: item.id,
name: $row.find(".studentname").attr("value")
};
$.ajax({
url: "api/values/" + item.id,
type: "PUT",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(student),
success: function() {
url: "api/values/" + item.id,
type: "PUT",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(student),
success: function() {
}
});
});
$students.append($row);
});
}
});
});
function addStudent()
{
var student = {
name: $("#frm").find("#name").attr("value"),
};
$.ajax({
url: "api/values",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(student),
success: function() {
alert("Added!");
}
});
}
</script>
</div>
</section>
<section class="content-wrapper main-content
clear-fix">
<div id="templates"
style="display:none">
<table>
<tr class="row-template">
<form>
<td class="Name"></td>
<td><input
type="button" value ="Change"
class="change"/></td>
<td><input type="button" value ="Del"
class="delete"/></td>
</form>
</tr>
</table>
</div>
<form id="frm">
<input type="text" name="name" id="name"/>
<td><input type="button" value ="Add"
onclick="return addStudent();"/></td>
</form>
</section>
</div>
</section>
</div>
让我们通过 curl 和 Fiddler 模拟创建、更新和删除操作的行为。请注意,在 curl 中,消息正文的创建方式很特别:如果我们有一个复杂的模型,那么我们就输入 name=Sychev,如果我们有一个简单的模型只有一个字段,那么我们就必须使用 -d "=Sychev"。
POST
PUT

删除
如果我们作为响应将错误消息返回给客户端,就会发生这种情况。
恭喜,我们使用 ASP.NET MVC 和 jQuery 的 Web API REST 服务已完成。现在让我们看看一些附加功能。
Web API 中的 OData 支持ASP.NET Web API 内置支持 OData 查询的几种 参数,例如排序、过滤和分页。
Web API 中的 OData 使用从 NuGet 包安装包含一组依赖项的包开始。
将 Get 方法的内容替换为相应的代码。
[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)] public IQueryable<Student> Get() { return _ctx.Student.AsQueryable(); }
要使用 OData 方法,我们需要将其标记为 [Queryable] 并告诉它返回 IQueryable。稍后将以一种稍有不同的方式进行 OData 数据序列化。
现在,让我们在 App_Start 文件夹中的 WebApiConfig 文件中添加一些“糖”,稍后会讨论。WebApiConfig 代码应该与下面提供的代码一致。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { GlobalConfiguration.Configuration.Formatters.JsonFormatter.AddQueryStringMapping("$format", "json", "application/json"); GlobalConfiguration.Configuration.Formatters.XmlFormatter.AddQueryStringMapping("$format", "xml", "application/xml"); GlobalConfiguration.Configuration.Formatters.XmlFormatter.AddQueryStringMapping("$format", "xml", "application/xml"); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); }
让我们运行项目并访问 https://:[port]/api/values。将打印出 OData 返回的 XML。
请注意,数据序列化的方式略有不同,在开发时应予以考虑。
现在我们可以利用 OData 提供的机会。例如,访问链接
https://:61020/api/values?$filter=(id eq 6)
或者
https://:61020/api/values?$filter=(id eq 6)&$format=jsonFormat 是一个非标准过程,我们已将其添加到 WebApiConfig 文件中。
使用 OData,我们可以通过格式与服务器端存储的数据模型进行交互。当然,尽管 Web API OData 提供只读访问数据,但我们必须小心。
感谢您的关注。