Code Copied

【译】Web API系列教程(二)—在Web API 2中实现CURD操作

CURD表示”创建、更新、读取和删除“这4个基本的数据库操作。很多HTTP服务也通过REST或者类似REST的API来模型化CURD操作。

在本教程中,我们将构建一个非常简单的产品Web API来管理Product。每个Product包含ID、名称、价格和类别属性。

1. Resource和HTTP Method

本教程中的Product API将会公开以下方法:

Action HTTP method Relative URI
获取所有的Product GET /api/products
根据ID获取Product GET /api/products/id
根据类别获取Product GET /api/products?category=category
创建一个新Product POST /api/products
更新一个Product PUT /api/products/id
删除一个Product DELETE /api/products/id


请注意一些URI路径中包括Product ID,若要获取ID 是 28 的Product,则客户端需要发送一个 GET 请求到http://hostname/api/products/28

1.1 Resource

Product API定义了2种Resource类型的URI:

Resource URI
所有的Product /api/products
单个的Product /api/products/id

1.2 HTTP Method

Web API的CURD的操作可以映射成4种主要的HTTP Method,分别是GET、POST、PUT和DELETE:

  • GET根据指定的URI获取Resource展示。
  • POST用于创建一个新的Resource。服务器给新的对象分配URI,并返回URI作为响应信息的一部分。
  • PUT根据指定的URI更新Resource。如果服务器允许客户端指定新的URI,PUT也可以根据一个指定的URI创建一个新的Resource。
  • DELETE根据制定的URI删除Resource。

注意:PUT方法在更新时将会替换整个Product实体。这表明客户端将发送一个完整的Product实例(包括所有Product的所有属性)到服务端。如果想支持部分更新,则可以选择PATCH方法。

2. 创建Web API项目

2.1 创建一个新的Web API项目

启动Visual Studio,在Start Page或者File Menu中选择New Project

Templates面板,选择Installed Templates并展开Visual C#节点。在Visual C#节点所列举的项目模板中选择Web,然后选择ASP.NET Web Application。给将项目命名为“ProductStore”,然后点击OK

SNAGHTML1bf7fb3

在弹出的New ASP.NET Project对话框中,选择Web API,点击OK

SNAGHTML1c230c9

2.2 添加一个Model

右键选中Models文件夹,点击Add,在弹出的菜单中选择Class,输入Class的名称,点击OK

image

Product的属性如下:

namespace ProductStore.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

2.3 添加一个Repository

现在我们需要实现Product的数据存储操作,一种较好的设计方式是将数据存储和服务实现分离,这时我们可以借助于Repository模式来实现。Repository模式的好处在于当我们更改存储时,我们无需更改服务类,存储层和服务层之间是低耦合的。

右键选中Models文件夹,点击Add,在弹出的菜单中选择New Item

image

Templates面板,选择Installed Templates并展开Visual C#节点。在Visual C#节点所列举的模板列表中选择Interface,将Interface命名为IProductRepository。

SNAGHTML1d8c3a5

在IProductRepository添加如下一些方法:

using System.Collections.Generic;

namespace ProductStore.Models
{
    public interface IProductRepository
    {
        IEnumerable<Product> GetAll();
        Product Get(int id);
        Product Add(Product item);
        void Remove(int id);
        bool Update(Product item);
    }
}

在Models文件夹中添加类ProductRepository,这个类将会实现IProductRepository接口,具体实现如下:

public class ProductRepository : IProductRepository
    {
        private List<Product> products = new List<Product>();
        private int _nextId = 1;

        public ProductRepository()
        {
            Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M });
            Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M });
            Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M });
        }

        public IEnumerable<Product> GetAll()
        {
            return products;
        }

        public Product Get(int id)
        {
            return products.Find(p => p.Id == id);
        }

        public Product Add(Product item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            item.Id = _nextId++;
            products.Add(item);
            return item;
        }

        public void Remove(int id)
        {
            products.RemoveAll(p => p.Id == id);
        }

        public bool Update(Product item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            int index = products.FindIndex(p => p.Id == item.Id);
            if (index == -1)
            {
                return false;
            }
            products.RemoveAt(index);
            products.Add(item);
            return true;
        }
    }

Repository将数据存储在内存中,用这种方式作为教程是可以的,但是在实际应用中应将数据存储在外部媒介中,例如数据库或者云存储。

2.4 添加一个Web API Controller

在Controllers文件夹下已经有2个Controller了,HomeController和ValueController。

  • HomeController是一个传统的ASP.NET MVC Controller。它一般作为站点的主要入口,和Web API也没有直接的关系。
  • ValuesController仅仅是一个Web API示例Controller。

本教程中ValuesController没有什么作用,我们将之删除,并添加一个新的Controller。

image

在弹出的列表中,选择Web API Controller – Empty

SNAGHTML1e80891

将Controller命名为ProductsController。

SNAGHTML1e894d9

Controller文件创建完成后打开它,添加如下引用:

添加一个包含 IProductRepository 实例的字段。

在Controller中调用new ProductRepository()并不是最好的设计,因为Controller依赖于IProductRepository的特定实现,即ProductRepository类。
根据S.O.L.I.D.原则中的DIP(依赖倒置原则),Controller应该依赖于抽象,即IProductRepository接口。更好的方式请参考Using the Web API Dependency Resolver

3. CURD实现

3.1 获取Resource

ProductStore API将一些”read”操作以HTTP GET的方式公开,每一个操作在ProductsController都有对应的方法。

Action HTTP method Relative URI
获取所有的Product GET /api/products
根据ID获取Product GET /api/products/id
根据类别获取Product GET /api/products?category=category

如果要获取所有的产品,则在ProductController中添加如下方法:

GetAllProducts方法名称以”Get“开始,所以这个方法会映射到GET请求。同样地,由于这个方法没有参数,所以它映射的URI路径中将不包含id字段。所以该方法应色的URI是/api/products

如果要根据ID获取Product,则在ProductController中添加如下方法:

这个方法的名字也开始以"Get",但该方法具有一个名为 id 的参数。此参数映射到 URI 路径的"id"部分。ASP.NET Web API 框架会自动将 ID 转换为正确的数据类型 (int) 的参数。

如果id参数无效,GetProduct方法将引发HttpResponseException异常。这个异常将会被ASP.NET Framework转译成一个404(未找到)错误。

最后,在ProductsController中添加一个根据类别查找产品的方法:

如果请求的URI包含查询字符串,Web API将尝试根据参数匹配ProductsController中的方法。因此,"api/products?category=category" 将会匹配GetProductsByCategory方法。

3.2 创建Resource

接下来,我们将在ProductsController中添加一个创建Product的方法。下面是一个简单的实现:

关于这个方法请注意两点:

  • 方法名称以”Post…”开始。要创建一个新的Product,客户端则需要发送一个HTTP POST请求。
  • 方法中包含一个Product参数。在Web API中,复杂类型的参数是在请求体(request body)中通过反序列化获取的。因此,我们期望客户端在请求时发送 XML 或 JSON 格式的产品对象。

这个简单的实现也能够工作,但它不是很完善。理想情况下,我们想要的HTTP Reponse应该包括以下内容:

  • Response code:默认情况下,Web API 将响应状态代码设置为 200 (OK)。但根据 HTTP/1.1 协议,当 POST 请求中创建了一个资源,服务器应该回复状态 201 (已创建)。
  • Location: 当服务器创建一个新的资源时,它应该在位置标头中包含新资源的URI。

ASP.NET Web API已经让操作HTTP响应消息变得很简单了,下面是PostProduct方法的改进实现:

请注意此方法返回类型现在是 HttpResponseMessage。通过返回 HttpResponseMessage 而不是产品,我们可以控制HTTP 响应消息的详细信息,包括状态代码和位置标头。

CreateResponse方法创建HttpResponseMessage,并自动将Product对象的序列化后写入到响应消息体。

这个例子中没有验证Product,有关模型验证的信息,请看Model Validation in ASP.NET Web API

3.3 更新Resource

通过PUT方式更新一个Product:

这个方法名称以“Put..”开始,所以Web API将其匹配到PUT请求。该方法有2个参数,product ID和更新的product对象。Id参数取自URI路径,product参数则从请求体中通过反序列化获得。默认情况下,ASP.NET Web API从路由中获取简单类型参数(id),从请求体中获取复杂参数(product对象)。

3.4 删除Resource

如果要删除一个Product,则定义一个“Delete…”方法。

如果删除请求成功,它将返回状态200(OK)和一个描述该状态的实体正文。
如果删除仍在执行,则返回状态202(Accepted)。
否则返回状态204(No Content)和一个没有实体的正文,在这种情况下,DeleteProduct方法��有一个void返回类型,所以ASP.NET Web API自动将其转化为状态码204。

3.5 全部源码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using ProductStore.Models;

namespace ProductStore.Controllers
{
    public class ProductsController : ApiController
    {
        static readonly IProductRepository repository = new ProductRepository();

        /// <summary>
        /// HTTP GET获取所有产品
        /// URI:/api/products
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Product> GetAllProducts()
        {
            return repository.GetAll();
        }

        /// <summary>
        /// HTTP GET根据ID获取产品
        /// URI:/api/products/id
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public Product GetProduct(int id)
        {
            return repository.Get(id);
        }

        /// <summary>
        /// HTTP GET根据类别获取产品
        /// URI:/api/products?category=category
        /// </summary>
        /// <param name="category"></param>
        /// <returns></returns>
        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return
                repository.GetAll().Where(p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase));
        }

        /// <summary>
        /// HTTP POST添加一个产品
        /// URI:/api/products
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public HttpResponseMessage PostProduct(Product item)
        {
            item = repository.Add(item);
            var reponse = Request.CreateResponse<Product>(HttpStatusCode.Created, item);

            string uri = Url.Link("DefaultApi", new {id = item.Id});
            reponse.Headers.Location = new Uri(uri);
            return reponse;
        }

        /// <summary>
        /// HTTP PUT更新一个产品
        /// URI:/api/products/id
        /// </summary>
        /// <param name="id"></param>
        /// <param name="product"></param>
        public void PutProduct(int id, Product product)
        {
            product.Id = id;
            if (!repository.Update(product))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

        /// <summary>
        /// HTTP DELETE删除一个产品
        /// URI:/api/products/id
        /// </summary>
        /// <param name="id"></param>
        public void DeleteProduct(int id)
        {
            Product item = repository.Get(id);
            if (item == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            repository.Remove(id);
        }
    }
}

通过整体的源代码,我们不难发现GetProduct(HTTP GET)、PutProduct(HTTP PUT)、DeleteProduct(HTTP Delete)方法共用了一个URI:/api/products/id,但是通过不同的HTTP Method我们能够区分它们的不同之处。

4. 总结、参考和下载

4.1 总结

本文简单介绍了如何在Web API中实现资源的CURD操作,Web API中的CURD操作也采用了契约式设计(Design by Contract):

  • 在ApiController中定义“Get..”方法时,体现为read操作,并映射为HTTP的GET方法。
  • 在ApiController中定义“Post..”方法时,体现为create操作,并映射为HTTP的POST方法。
  • 在ApiController中定义”Put…”方法时,体现为update操作,并映射为HTTP的PUT方法。
  • 在ApiController中定义“Delete…”方法时,体现为delete操作,并映射为HTTP的DELETE方法。

4.2 参考和下载

参考链接

Creating a web api that supports CRUD operations

HTTP Methods

下载

完整工程文件:http://blog.64cm.com/source/WebAPI/ProductStore.zip