HTTP协议涉及到两个重要的性质:幂等性和安全性,相应的HTTP方法称为幂等方法和安全方法,具有幂等性质的方法不一定是安全的方法。举个例子GET方法是幂等方法,同时也是安全方法,PUT是幂等方法,但不是安全方法。

在HTTP/1.1规范(http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1)中幂等性的定义是:

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴,正如编译器只能帮助检查语法错误一样,HTTP规范也没有办法通过消息格式等语法手段来定义它,这可能是它不太受到重视的原因之一。但实际上,幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。

安全的HTTP方法可以理解为对服务器没有任何副作用,比如GET方法只是请求资源,HEAD方法只是请求资源的响应消息报头,DELETE方法则是删除了指定的资源,所以DELETE方法不是安全的方法,而GET和HEAD是安全的方法。

文章接下来的内容是实例讲解如何构建符合HTTP这两个重要性质的Web API。

获取资源

HTTP GET方法用于获取资源,例如http://server/api/employees用于获取所有的员工,而http://server/api/employees/12345获取一个特定的员工。GET方法没有请求主体,响应主体是资源的JSON或XML表示,也可以自定义成其他格式的表示,后面文章会介绍如何自定义。使用GET方法一定要记住,不要在GET方法中实现改变系统状态的逻辑。

还是引用前面文章的例子,根据员工id获取员工,本例中稍微修改一下:

<br />
        public Employee Get(int id)<br />
        {<br />
            var employee = list.FirstOrDefault(e =&gt; e.Id == id);<br />
            if (employee == null)<br />
            {<br />
                throw new HttpResponseException(HttpStatusCode.NotFound);<br />
            }<br />
            return employee;<br />
        }<br />

本例中,如果员工不存在,则返回404 - Not Found。选择合适的HTTP状态码也是构建符合HTTP规范服务的重要方面。

下面的例子是获取部门的所有员工:

<br />
        public IEnumerable&lt;Employee&gt; GetByDepartment(int department)<br />
        {<br />
            int[] validDepartments = { 1, 2, 3, 5, 8, 13 };<br />
            if (!validDepartments.Any(d =&gt; d == department))<br />
            {<br />
                var response = new HttpResponseMessage()<br />
                {<br />
                    StatusCode = (HttpStatusCode)422, // Unprocessable Entity<br />
                    ReasonPhrase = &quot;Invalid Department&quot;<br />
                };<br />
                throw new HttpResponseException(response);<br />
            }<br />
            return list.Where(e =&gt; e.Department == department);<br />
        }<br />

本例与上个例子稍微有点不同,上个例子中直接抛出状态码,本例中则构建了HttpResponseMessage抛出,这样的话可以发送出现异常的原因。

当然还可以根据多个条件查询,比如要获取某个部门并且最后一个名字是某个字的所有员工。实现多条件查询,首先要把条件定义成一个类,我们称之为Filter类:

<br />
    public class Filter<br />
    {<br />
        public int Department { get; set; }<br />
        public string LastName { get; set; }<br />
    }<br />

相应的Action为:

<br />
        public IEnumerable&lt;Employee&gt; Get([FromUri]Filter filter)<br />
        {<br />
            return list.Where(e =&gt; e.Department == filter.Department &amp;&amp;<br />
            e.LastName == filter.LastName);<br />
        }<br />

这里需要注意的是,Filter类必须使用FromUri特性。

创建服务器生成标识符的资源

创建一个employee,可以对URI:http://localhost:port/api/employees发送HTTP POST请求,在URI中没有指定ID。在本例中请求主体是一个新员工的JSON或XML表示, 响应主体是新创建员工的JSON或XML表示,两者的区别在于请求主体中不包括ID,响应主体中包括服务器生成的ID。

下面的例子,我们返回一个HttpResponseMessage类型的对象,这样可以更好的控制返回客户端的信息,包括HTTP状态码。使用POST创建资源,返回HTTP状态码201 - Created比返回200 - OK更符合HTTP/1.1规范。

<br />
        public HttpResponseMessage Post(Employee employee)<br />
        {<br />
            int maxId = list.Max(e =&gt; e.Id);<br />
            employee.Id = maxId + 1;<br />
            list.Add(employee);<br />
            var response = Request.CreateResponse&lt;Employee&gt;(HttpStatusCode.Created, employee);<br />
            string uri = Url.Link(&quot;DefaultApi&quot;, new { id = employee.Id });<br />
            response.Headers.Location = new Uri(uri);<br />
            return response;<br />
        }<br />

发送一个POST请求需要像Fiddler这样的工具,在下一篇文章中会讲Fiddler的使用。

这里需要注意的是,使用HTTP POST创建资源,在URI中一定不要包含ID,应该这样:http://localhost:port/api/employees。使用带有ID的URI发送POST请求,如http://localhost:port/api/employees/12348可以用来更新资源,如果12348是不存在的ID,一定要使用404 -Not Found拒绝。

创建客户端提供标识符的资源

可以对URI:http://localhost:port/api/employees/12348发送HTTP PUT请求创建一个员工资源。本例中,请求主体包含员工资源的JSON或XML表示,响应主体也是该员工的JSON或XML表示,但是响应主体可以忽略,因为两者之间没有什么区别。

<br />
        public HttpResponseMessage Put(int id, Employee employee)<br />
        {<br />
            if (!list.Any(e =&gt; e.Id == id))<br />
            {<br />
                list.Add(employee);<br />
                var response = Request.CreateResponse&lt;Employee&gt;<br />
                (HttpStatusCode.Created, employee);<br />
                string uri = Url.Link(&quot;DefaultApi&quot;, new { id = employee.Id });<br />
                response.Headers.Location = new Uri(uri);<br />
                return response;<br />
            }<br />
            return Request.CreateResponse(HttpStatusCode.NoContent);<br />
        }<br />

使用HTTP PUT创建资源适用于客户端提供ID的情况。

重写资源

可以使用HTTP PUT重写资源。这里重写资源和更新资源有点区别。发送PUT请求,在请求主体中必须是一个资源的完整表示,而不是一部分。PUT请求的URI如:http://localhost:port/api/employees/12345,ID为12345的员工是已经在系统中存在的。

<br />
        public HttpResponseMessage Put(int id, Employee employee)<br />
        {<br />
            int index = list.ToList().FindIndex(e =&gt; e.Id == id);<br />
            if (index &gt;= 0)<br />
            {<br />
                list[index] = employee; // overwrite the existing resource<br />
                return Request.CreateResponse(HttpStatusCode.NoContent);<br />
            }<br />
            else<br />
            {<br />
                list.Add(employee);<br />
                var response = Request.CreateResponse&lt;Employee&gt;<br />
                (HttpStatusCode.Created, employee);<br />
                string uri = Url.Link(&quot;DefaultApi&quot;, new { id = employee.Id });<br />
                response.Headers.Location = new Uri(uri);<br />
                return response;<br />
            }<br />
        }<br />

 更新资源

对URI:http://server/api/employees/12345发送HTTP POST请求可以更新资源。如果没有与传入的ID匹配的员工,则返回404 - Not Found拒绝请求。

<br />
        public HttpResponseMessage Post(int id, Employee employee)<br />
        {<br />
            int index = list.ToList().FindIndex(e =&gt; e.Id == id);<br />
            if (index &gt;= 0)<br />
            {<br />
                list[index] = employee;<br />
                return Request.CreateResponse(HttpStatusCode.NoContent);<br />
            }<br />
            return Request.CreateResponse(HttpStatusCode.NotFound);<br />
        }<br />

HTTP POST支持部分更新资源,但是有一个专门的方法PATCH用于部分更新。

部分更新资源

在应用部分更新时,存在的问题是:ASP.NET Web API框架会反序列化请求中资源的表示为参数对象,但是区分一个字段是null,还是没有提供不是那么容易的。举个例子,假设Employee类型有一个属性Age,它定义为int?。如果不想更新这个属性,可以在请求主体中不包含这个属性,那么在反序列化时,这个Age就会反序列化为null。如果想更新Age为null,那么可以在请求主体中设置Age为null,这样在反序列化时Age也会反序列化为null。使用POST区分两者的不同来部分更新是有些困难的。

不过不用担心,Microsoft ASP.NET Web API OData有动态代理Delta<T>,它可以跟踪ASP.NET Web API反序列化的对象和持久存储的对象之间的不同。下面给出使用Delta<T>的例子:

在NuGet程序包管理器中搜索“odata”,如下图所示:

ASP.NET Web API系列——构建符合HTTP规则的Web API-程序旅途

Delta<T>在System.Web.OData命名空间中。

<br />
        public HttpResponseMessage Patch(int id, Delta&lt;Employee&gt; deltaEmployee)<br />
        {<br />
            var employee = list.FirstOrDefault(e =&gt; e.Id == id);<br />
            if (employee == null)<br />
            {<br />
                throw new HttpResponseException(HttpStatusCode.NotFound);<br />
            }<br />
            deltaEmployee.Patch(employee);<br />
            return Request.CreateResponse(HttpStatusCode.NoContent);<br />
        }<br />

 删除资源

HTTP DELETE也是幂等方法。

<br />
        public void Delete(int id)<br />
        {<br />
            Employee employee = Get(id);<br />
            list.Remove(employee);<br />
        }<br />

如果不是立即删除,而是将它标记为删除,在稍后的时间点再删除,不要发送200 - OK 或204 -No Content作为对DELETE请求的响应,而应该发送202 - Accepted。

本篇参考《Practical ASP.NET Web API》一书的1.4 Playing by the Rules of HTTP

示例代码在这里:https://github.com/mingceng/web-api-demo