在 ASP.NET Web API 1 中启用 CRUD 操作

作者:Mike Wasson

下载已完成项目

本教程介绍如何使用适用于 ASP.NET 4.x 的 ASP.NET Web API 在 HTTP 服务中支持 CRUD 操作。

本教程中使用的软件版本

  • Visual Studio 2012
  • Web API 1 (也适用于 Web API 2)

CRUD 代表“创建、读取、更新和删除”,这是四个基本数据库操作。 许多 HTTP 服务还通过 REST 或类似于 REST 的 API 为 CRUD 操作建模。

在本教程中,你将构建一个非常简单的 Web API 来管理产品列表。 每个产品将包含名称、价格和类别 (,例如“玩具”或“硬件”) ,以及产品 ID。

产品 API 将公开以下方法。

操作 HTTP 方法 相对 URI
获取所有产品的列表 GET /api/products
根据 ID 获取产品 GET /api/products/id
按类别获取产品 GET /api/products?category=category
创建新产品 POST /api/products
更新产品 PUT /api/products/id
删除产品 DELETE /api/products/id

请注意,某些 URI 在路径中包含产品 ID。 例如,若要获取 ID 为 28 的产品,客户端会发送针对 的 http://hostname/api/products/28GET 请求。

资源

产品 API 定义了两种资源类型的 URI:

资源 URI
所有产品的列表。 /api/products
单个产品。 /api/products/id

方法

(GET、PUT、POST 和 DELETE) 的四main HTTP 方法可以映射到 CRUD 操作,如下所示:

  • GET 检索指定 URI 处的资源表示形式。 GET 应该不会对服务器产生任何副作用。
  • PUT 更新指定 URI 处的资源。 如果服务器允许客户端指定新的 URI,则 PUT 还可用于在指定的 URI 处创建新资源。 在本教程中,API 不支持通过 PUT 创建。
  • POST 创建新资源。 服务器为新对象分配 URI,并将此 URI 作为响应消息的一部分返回。
  • DELETE 删除指定 URI 处的资源。

注意:PUT 方法替换整个产品实体。 也就是说,客户端应发送更新产品的完整表示形式。 如果要支持部分更新,首选 PATCH 方法。 本教程不实现 PATCH。

创建新的 Web API 项目

首先运行 Visual Studio,然后从“开始”页中选择“新建项目”。 或者,从“ 文件 ”菜单中选择“ 新建 ”,然后选择“ 项目”。

“模板 ”窗格中,选择“ 已安装的模板 ”,然后展开 “Visual C# ”节点。 在 “Visual C#”下,选择“ Web”。 在项目模板列表中,选择“ ASP.NET MVC 4 Web 应用程序”。 将项目命名为“ProductStore”,然后单击“ 确定”。

“新建项目”窗口的屏幕截图,其中显示了菜单选项并突出显示了用于创建 A S P dot NET M V C 4 Web 应用程序的路径。

“新建 ASP.NET MVC 4 项目 ”对话框中,选择“ Web API ”并单击“ 确定”。

新的 A S P 点 NET 项目的屏幕截图,其中显示了可用模板的盒装图像,并突出显示了 Web A P I 模板(以蓝色显示)。

添加模型

模型是表示应用程序中的数据的对象。 在 ASP.NET Web API 中,可以使用强类型 CLR 对象作为模型,它们将自动序列化为客户端的 XML 或 JSON。

对于 ProductStore API,我们的数据由产品组成,因此我们将创建一个名为 Product的新类。

如果解决方案资源管理器尚未出现,请单击“视图”菜单,并选择“解决方案资源管理器”。 在“解决方案资源管理器”中,右键单击“模型”文件夹。 在上下文菜单中选择“ 添加”,然后选择“ ”。 将类命名为“Product”。

解决方案资源管理器菜单的屏幕截图,突出显示了模型的选择,以显示用于选择“添加类”选项的其他菜单。

将以下属性添加到 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; }
    }
}

添加存储库

我们需要存储一系列产品。 最好将集合与服务实现分开。 这样,我们可以更改后备存储,而无需重写服务类。 这种类型的设计称为 存储库 模式。 首先定义存储库的泛型接口。

在“解决方案资源管理器”中,右键单击“模型”文件夹。 选择 “添加”,然后选择“ 新建项”。

解决方案资源管理器菜单的屏幕截图,其中突出显示了“模型”选项并显示用于添加新项的菜单。

“模板 ”窗格中,选择“ 已安装的模板 ”,然后展开“C#”节点。 在“C#”下,选择“ 代码”。 在代码模板列表中,选择“ 接口”。 将接口命名为“IProductRepository”。

模板窗格的屏幕截图,其中显示了已安装的模板菜单,其中突出显示了灰色的代码和界面选项。

添加以下实现:

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);
    }
}

现在,将另一个名为“ProductRepository”的类添加到 Models 文件夹。此类将实现 IProductRepository 接口。 添加以下实现:

namespace ProductStore.Models
{
    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;
        }
    }
}

存储库将列表保存在本地内存中。 这在教程中是可以的,但在实际应用程序中,可以将数据存储在外部,无论是数据库还是云存储中。 存储库模式将使稍后更改实现变得更容易。

添加 Web API 控制器

如果曾使用过 ASP.NET MVC,则已熟悉控制器。 在 ASP.NET Web API 中,控制器是处理来自客户端的 HTTP 请求的类。 新建项目向导在创建项目时为你创建了两个控制器。 若要查看它们,请展开 解决方案资源管理器 中的 Controllers 文件夹。

  • HomeController 是传统的 ASP.NET MVC 控制器。 它负责为网站提供 HTML 页面,与我们的 Web API 不直接相关。
  • ValuesController 是一个示例 WebAPI 控制器。

继续并删除 ValuesController,方法是右键单击解决方案资源管理器中的文件,然后选择“删除”。现在添加新控制器,如下所示:

在“解决方案资源管理器”中,右键单击“控制器”文件夹。 依次选择“添加”、“控制器”。

解决方案资源管理器菜单的屏幕截图,其中突出显示了控制器类别,该类别带来了另一个菜单,突出显示了添加控制器的路径。

“添加控制器 ”向导中,将控制器命名为“ProductsController”。 在 “模板” 下拉列表中,选择“ 空 API 控制器”。 然后单击“添加” 。

“添加控制器”窗口的屏幕截图,其中显示了用于输入名称的控制器名称字段,以及基架选项下的模板下拉列表。

注意

无需将控制器放入名为 Controllers 的文件夹中。 文件夹名称并不重要;它只是组织源文件的便捷方法。

添加控制器向导将在 Controllers 文件夹中创建名为 ProductsController.cs 的文件。 如果此文件尚未打开,请双击文件将其打开。 添加以下 using 语句:

using ProductStore.Models;

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

public class ProductsController : ApiController
{
    static readonly IProductRepository repository = new ProductRepository();
}

注意

在控制器中调用 new ProductRepository() 不是最佳设计,因为它将控制器与 的特定实现 IProductRepository联系起来。 有关更好的方法,请参阅 使用 Web API 依赖项解析程序

获取资源

ProductStore API 会将多个“读取”操作作为 HTTP GET 方法公开。 每个操作都将对应于 类中的 ProductsController 一个方法。

操作 HTTP 方法 相对 URI
获取所有产品的列表 GET /api/products
根据 ID 获取产品 GET /api/products/id
按类别获取产品 GET /api/products?category=category

若要获取所有产品的列表,请将此方法添加到 ProductsController 类:

public class ProductsController : ApiController
{
    public IEnumerable<Product> GetAllProducts()
    {
        return repository.GetAll();
    }
    // ....
}

方法名称以“Get”开头,因此按照约定会映射到 GET 请求。 此外,由于 方法没有参数,因此它映射到路径中不包含 “id” 段的 URI。

若要按 ID 获取产品,请将此方法添加到 ProductsController 类:

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound); 
    }
    return item;
}

此方法名称也以“Get”开头,但该方法具有名为 id 的参数。此参数映射到 URI 路径的“id”段。 ASP.NET Web API框架会自动将 ID 转换为参数的正确数据类型 (int) 。

如果 id 无效,GetProduct 方法将引发 HttpResponseException 类型的异常。 此异常将由框架转换为 404 (找不到) 错误。

最后,添加方法以按类别查找产品:

public IEnumerable<Product> GetProductsByCategory(string category)
{
    return repository.GetAll().Where(
        p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase));
}

如果请求 URI 具有查询字符串,Web API 会尝试将查询参数与控制器方法上的参数匹配。 因此,格式为“api/products?category=category”的 URI 将映射到此方法。

创建资源

接下来,我们将向 类添加一个方法来 ProductsController 创建新产品。 下面是 方法的简单实现:

// Not the final implementation!
public Product PostProduct(Product item)
{
    item = repository.Add(item);
    return item;
}

请注意此方法的两点:

  • 方法名称以“Post...”开头。 若要创建新产品,客户端会发送 HTTP POST 请求。
  • 方法采用 Product 类型的参数。 在 Web API 中,具有复杂类型的参数将从请求正文反序列化。 因此,我们希望客户端以 XML 或 JSON 格式发送产品对象的序列化表示形式。

此实现将起作用,但并不十分完整。 理想情况下,我们希望 HTTP 响应包括以下内容:

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

ASP.NET Web API可以轻松操作 HTTP 响应消息。 下面是改进的实现:

public HttpResponseMessage PostProduct(Product item)
{
    item = repository.Add(item);
    var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item);

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

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

CreateResponse 方法创建 HttpResponseMessage,并自动将 Product 对象的序列化表示形式写入响应消息正文中。

注意

此示例不验证 Product。 有关模型验证的信息,请参阅 ASP.NET Web API 中的模型验证

更新资源

使用 PUT 更新产品非常简单:

public void PutProduct(int id, Product product)
{
    product.Id = id;
    if (!repository.Update(product))
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
}

方法名称以“Put...”开头,因此 Web API 将其与 PUT 请求匹配。 方法采用两个参数:产品 ID 和更新的产品。 id 参数取自 URI 路径,product 参数从请求正文反序列化。 默认情况下,ASP.NET Web API框架采用路由中的简单参数类型和请求正文中的复杂类型。

删除资源

若要删除资源,请定义“删除...”方法。

public void DeleteProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    repository.Remove(id);
}

如果 DELETE 请求成功,可以使用描述状态的实体正文返回状态 200 (正常) ;状态 202 (如果删除仍处于挂起状态,则) 已接受;或状态 204 (无实体正文的无内容) 。 在这种情况下, DeleteProduct 方法具有void返回类型,因此 ASP.NET Web API自动将其转换为状态代码 204 (无内容) 。