使用 ASP.NET Web API 创建 OData v4 终结点

开放数据协议 (OData) 是 Web 的数据访问协议。 OData 通过 CRUD 操作提供统一的方法来查询和操作数据集, (创建、读取、更新和删除) 。

ASP.NET Web API支持协议的 v3 和 v4。 甚至可以有一个与 v3 终结点并行运行的 v4 终结点。

本教程介绍如何创建支持 CRUD 操作的 OData v4 终结点。

本教程中使用的软件版本

  • Web API 5.2
  • OData v4
  • Visual Studio 2017 (在此处 下载 Visual Studio 2017)
  • Entity Framework 6
  • .NET 4.7.2

教程版本

有关 OData 版本 3,请参阅 创建 OData v3 终结点

创建 Visual Studio 项目

在 Visual Studio 的“ 文件 ”菜单中,选择“ 新建>项目”。

展开“已安装>的 Visual C#>Web”,然后选择“ASP.NET Web 应用程序 (.NET Framework) ”模板。 将项目命名为“ProductService”。

Visual Studio 新建项目窗口的屏幕截图,其中显示了使用 dot NET Framework 创建 A S P dot NET Web 应用程序的菜单选项。

选择“确定” 。

A S P dot NET Web 应用程序的屏幕截图,其中显示了可用于创建包含 Web A P I 文件夹和核心引用的应用程序的模板。

选择“空”模板。“为:添加文件夹和核心引用”下,选择“ Web API”。 选择“确定” 。

安装 OData 包

在“工具”菜单中,选择“NuGet 包管理器”>“包管理器控制台”。 在“包管理器控制台”窗口中,键入:

Install-Package Microsoft.AspNet.Odata

此命令安装最新的 OData NuGet 包。

添加模型类

模型是一个对象,表示应用程序中的数据实体。

在解决方案资源管理器中,右键单击“模型”文件夹。 从上下文菜单中选择“ 添加>”。

解决方案资源管理器窗口的屏幕截图,其中突出显示了向项目添加模型类对象的路径。

注意

按照约定,模型类放置在 Models 文件夹中,但你不必在自己的项目中遵循此约定。

命名类 Product。 在 Product.cs 文件中,将样本代码替换为以下内容:

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

属性 Id 是实体键。 客户端可以按键查询实体。 例如,若要获取 ID 为 5 的产品,URI 为 /Products(5)。 属性 Id 也将是后端数据库中的主键。

启用实体框架

在本教程中,我们将使用 Entity Framework (EF) Code First 来创建后端数据库。

注意

Web API OData 不需要 EF。 使用可将数据库实体转换为模型的任何数据访问层。

首先,安装 EF 的 NuGet 包。 在“工具”菜单中,选择“NuGet 包管理器”>“包管理器控制台”。 在“包管理器控制台”窗口中,键入:

Install-Package EntityFramework

打开 Web.config 文件,在 configSections元素之后的配置元素中添加以下部分。

<configuration>
  <configSections>
    <!-- ... -->
  </configSections>

  <!-- Add this: -->
  <connectionStrings>
    <add name="ProductsContext" connectionString="Data Source=(localdb)\mssqllocaldb; 
        Initial Catalog=ProductsContext; Integrated Security=True; MultipleActiveResultSets=True; 
        AttachDbFilename=|DataDirectory|ProductsContext.mdf"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

此设置为 LocalDB 数据库添加连接字符串。 在本地运行应用时,将使用此数据库。

接下来,将名为 的 ProductsContext 类添加到 Models 文件夹:

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

在构造函数中, "name=ProductsContext" 提供连接字符串的名称。

配置 OData 终结点

打开文件 App_Start/WebApiConfig.cs。 添加以下 using 语句:

using ProductService.Models;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;

然后将以下代码添加到 Register 方法:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // New code:
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

此代码执行两项操作:

  • 创建实体数据模型 (EDM) 。
  • 添加路由。

EDM 是数据的抽象模型。 EDM 用于创建服务元数据文档。 ODataConventionModelBuilder 类使用默认命名约定创建 EDM。 此方法需要最少的代码。 如果想要对 EDM 进行更多控制,可以使用 ODataModelBuilder 类通过显式添加属性、键和导航属性来创建 EDM。

路由告知 Web API 如何将 HTTP 请求路由到终结点。 若要创建 OData v4 路由,请调用 MapODataServiceRoute 扩展方法。

如果应用程序有多个 OData 终结点,请为每个终结点创建单独的路由。 为每个路由指定唯一的路由名称和前缀。

添加 OData 控制器

控制器是处理 HTTP 请求的类。 为 OData 服务中的每个实体集创建单独的控制器。 在本教程中,将为实体创建一个控制器 Product

在“解决方案资源管理器”中,右键单击“控制器”文件夹,然后选择“添加>”。 命名类 ProductsController

注意

本教程的 OData v3 版本使用 添加控制器 基架。 目前,OData v4 没有基架。

将 ProductsController.cs 中的样本代码替换为以下内容。

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    public class ProductsController : ODataController
    {
        ProductsContext db = new ProductsContext();
        private bool ProductExists(int key)
        {
            return db.Products.Any(p => p.Id == key);
        } 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

控制器使用 ProductsContext 类通过 EF 访问数据库。 请注意,控制器重写 Dispose 方法以释放 ProductsContext

这是控制器的起点。 接下来,我们将为所有 CRUD 操作添加方法。

查询实体集

将以下方法添加到 ProductsController

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}

方法的 Get 无参数版本返回整个 Products 集合。 Get具有参数的方法按其密钥 (查找产品,在本例中, Id 属性) 。

[EnableQuery] 属性使客户端可以使用查询选项(如$filter、$sort和$page)修改查询。 有关详细信息,请参阅 支持 OData 查询选项

将实体添加到实体集

若要使客户端能够将新产品添加到数据库,请将以下方法添加到 ProductsController

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}

更新条目

OData 支持两种不同的语义来更新实体:PATCH 和 PUT。

  • PATCH 执行部分更新。 客户端仅指定要更新的属性。
  • PUT 替换整个实体。

PUT 的缺点是客户端必须发送实体中所有属性的值,包括未更改的值。 OData 规范指出,首选 PATCH。

在任何情况下,下面是 PATCH 和 PUT 方法的代码:

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var entity = await db.Products.FindAsync(key);
    if (entity == null)
    {
        return NotFound();
    }
    product.Patch(entity);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(entity);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}

在 PATCH 的情况下,控制器使用 Delta<T> 类型来跟踪更改。

删除条目

若要使客户端能够从数据库中删除产品,请将以下方法添加到 ProductsController

public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
    var product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return StatusCode(HttpStatusCode.NoContent);
}