使用 Web API 2 创建 OData v3 终结点

作者: Mike Wasson

下载完成的项目

Open Data Protocol (OData)是 web 的数据访问协议。 OData 提供一种统一的方式来构建数据、查询数据,以及通过 CRUD 操作(创建、读取、更新和删除)操作数据集。 OData 同时支持 AtomPub (XML)和 JSON 格式。 OData 还定义了公开数据相关元数据的方法。 客户端可以使用元数据来发现数据集的类型信息和关系。

ASP.NET Web API 使你可以轻松地为数据集创建 OData 终结点。 你可以精确控制终结点支持的 OData 操作。 可以托管多个 OData 终结点和非 OData 终结点。 您可以完全控制您的数据模型、后端业务逻辑和数据层。

本教程中使用的软件版本

ASP.NET 和 Web 工具2012.2 更新中添加了 Web API OData 支持。 不过,本教程使用在 Visual Studio 2013 中添加的基架。

在本教程中,你将创建一个客户端可以查询的简单 OData 终结点。 你还将为终结C#点创建客户端。 完成本教程后,下一组教程介绍如何添加更多功能,包括实体关系、操作和 $expand/$select。

创建 Visual Studio 项目

在本教程中,你将创建支持基本 CRUD 操作的 OData 终结点。 终结点将公开单个资源,即产品列表。 稍后的教程将添加更多功能。

启动 Visual Studio,然后从起始页中选择 "新建项目"。 或者,从 "文件" 菜单中选择 "新建",然后选择 "项目"。

在 "模板" 窗格中,选择 "已安装模板",然后展开视觉对象C#节点。 在 " C#视觉对象" 下选择 " Web"。 选择 "ASP.NET Web 应用程序" 模板。

在 "新建 ASP.NET 项目" 对话框中,选择 "" 模板。 在 "添加文件夹和核心引用 ...",检查WEB API。 单击“确定”。

添加实体模型

模型是表示应用程序中的数据的对象。 对于本教程,我们需要一个表示产品的模型。 模型对应于 OData 实体类型。

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

在 "添加新项" 对话框中,将类命名为 "产品"。

Note

按照约定,将模型类置于 model 文件夹中。 你不必在自己的项目中遵循此约定,但我们将在本教程中使用它。

在 Product.cs 文件中,添加以下类定义:

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 查询产品。 此字段还将是后端数据库中的主键。

立即生成项目。 在下一步中,我们将使用一些使用反射查找产品类型的 Visual Studio 基架。

添加 OData 控制器

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

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

在 "添加基架" 对话框中,选择 "包含操作 "Web API 2 OData 控制器",并使用实体框架"。

在 "添加控制器" 对话框中,将控制器命名为 "ProductsController"。 选中 ""使用异步控制器操作"" 复选框。 在 "模型" 下拉列表中,选择 "产品类"。

单击 "新建数据上下文 ... " 按钮。 保留数据上下文类型的默认名称,然后单击 "添加"。

在 "添加控制器" 对话框中单击 "添加" 以添加控制器。

注意:如果收到一条错误消息,提示 "获取类型时出错 ...",请确保在添加 Product 类后生成了 Visual Studio 项目。 基架使用反射来查找类。

基架将两个代码文件添加到项目中:

  • Products.cs 定义用于实现 OData 终结点的 Web API 控制器。
  • ProductServiceContext.cs 提供使用实体框架查询基础数据库的方法。

添加 EDM 和路由

在解决方案资源管理器中,展开 "应用_启动" 文件夹,然后打开名为 "WebApiConfig.cs" 的文件。 此类包含 Web API 的配置代码。 将此代码替换为以下代码:

using ProductService.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;

namespace ProductService
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
            builder.EntitySet<Product>("Products");
            config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
        }
    }
}

此代码执行两项操作:

  • 为 OData 终结点创建实体数据模型(EDM)。
  • 为终结点添加路由。

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

EntitySet方法将实体集添加到 EDM:

modelBuilder.EntitySet<Product>("Products");

字符串 "Products" 定义实体集的名称。 控制器的名称必须与实体集的名称匹配。 在本教程中,实体集命名为 "Products",控制器命名为 ProductsController。 如果已将该实体集命名为 "ProductSet",则会将该控制器命名为 ProductSetController。 请注意,终结点可以有多个实体集。 为每个实体集调用EntitySet<t> ,并定义相应的控制器。

MapODataRoute方法为 OData 终结点添加路由。

config.Routes.MapODataRoute("ODataRoute", "odata", model);

第一个参数是路由的友好名称。 你的服务的客户端看不到此名称。 第二个参数是终结点的 URI 前缀。 在给定此代码的情况下,Products 实体集的 URI 为 http://hostname/odata/Products。 你的应用程序可以有多个 OData 终结点。 对于每个终结点,调用MapODataRoute并提供唯一的路由名称和唯一的 URI 前缀。

播种数据库(可选)

在此步骤中,您将使用实体框架来为数据库提供一些测试数据。 此步骤是可选的,但它允许立即测试 OData 终结点。

从 "工具" 菜单中,选择 " NuGet 包管理器",然后选择 "程序包管理器控制台"。 在“Package Manager Console”窗口中,输入以下命令:

Enable-Migrations

这会添加一个名为迁移的文件夹和一个名为 Configuration.cs 的代码文件。

打开此文件,并将以下代码添加到 Configuration.Seed 方法。

protected override void Seed(ProductService.Models.ProductServiceContext context)
{
    // New code 
    context.Products.AddOrUpdate(new Product[] {
        new Product() { ID = 1, Name = "Hat", Price = 15, Category = "Apparel" },
        new Product() { ID = 2, Name = "Socks", Price = 5, Category = "Apparel" },
        new Product() { ID = 3, Name = "Scarf", Price = 12, Category = "Apparel" },
        new Product() { ID = 4, Name = "Yo-yo", Price = 4.95M, Category = "Toys" },
        new Product() { ID = 5, Name = "Puzzle", Price = 8, Category = "Toys" },
    });
}

在 "程序包管理器控制台" 窗口中,输入以下命令:

Add-Migration Initial
Update-Database

这些命令生成用于创建数据库的代码,然后执行该代码。

浏览 OData 终结点

在本部分中,我们将使用Fiddler Web 调试代理将请求发送到终结点并检查响应消息。 这将帮助你了解 OData 终结点的功能。

在 Visual Studio 中,按 F5 开始调试。 默认情况下,Visual Studio 会打开浏览器以 http://localhost:*port*,其中port是在项目设置中配置的端口号。

可以在项目设置中更改端口号。 在解决方案资源管理器中,右键单击项目,然后选择 "属性"。 在 "属性" 窗口中,选择 " Web"。 在 "项目 Url" 下输入端口号。

服务文档

服务文档包含 OData 终结点的实体集的列表。 若要获取服务文档,请将 GET 请求发送到服务的根 URI。

使用 Fiddler 在 "编辑器" 选项卡中输入以下 URI: http://localhost:port/odata/,其中port是端口号。

单击 "执行" 按钮。 Fiddler 将 HTTP GET 请求发送到应用程序。 应会在 "Web 会话" 列表中看到响应。 如果一切正常,状态代码将为200。

双击 "Web 会话" 列表中的响应,查看 "检查器" 选项卡中响应消息的详细信息。

原始 HTTP 响应消息应如下所示:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 17:51:01 GMT
Content-Length: 364

<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:60868/odata" 
    xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <workspace>
    <atom:title type="text">Default</atom:title>
    <collection href="Products">
        <atom:title type="text">Products</atom:title>
    </collection>
    </workspace>
</service></pre>

默认情况下,Web API 以 AtomPub 格式返回服务文档。 若要请求 JSON,请将以下标头添加到 HTTP 请求:

Accept: application/json

现在 HTTP 响应包含 JSON 有效负载:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 22:59:28 GMT
Content-Length: 136

{
  "odata.metadata":"http://localhost:60868/odata/$metadata","value":[
    {
      "name":"Products","url":"Products"
    }
  ]
}

服务元数据文档

服务元数据文档使用名为概念架构定义语言(CSDL)的 XML 语言描述服务的数据模型。 元数据文档显示服务中数据的结构,并可用于生成客户端代码。

若要获取元数据文档,请将 GET 请求发送到 http://localhost:port/odata/$metadata。 下面是本教程中所示的终结点的元数据。

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:05:52 GMT
Content-Length: 1086

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
  <edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0" 
    xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <Schema Namespace="ProductService.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
      <EntityType Name="Product">
        <Key>
          <PropertyRef Name="ID" />
        </Key>
        <Property Name="ID" Type="Edm.Int32" Nullable="false" />
        <Property Name="Name" Type="Edm.String" />
        <Property Name="Price" Type="Edm.Decimal" Nullable="false" />
        <Property Name="Category" Type="Edm.String" />
      </EntityType>
    </Schema>
    <Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
      <EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
        <EntitySet Name="Products" EntityType="ProductService.Models.Product" />
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>

实体集

若要获取 Products 实体集,请将 GET 请求发送到 http://localhost:port/odata/Products

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:01:31 GMT
Content-Length: 459

{
  "odata.metadata":"http://localhost:60868/odata/$metadata#Products","value":[
    {
      "ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
    },{
      "ID":2,"Name":"Socks","Price":"5.00","Category":"Apparel"
    },{
      "ID":3,"Name":"Scarf","Price":"12.00","Category":"Apparel"
    },{
      "ID":4,"Name":"Yo-yo","Price":"4.95","Category":"Toys"
    },{
      "ID":5,"Name":"Puzzle","Price":"8.00","Category":"Toys"
    }
  ]
}

实体

若要获取单个产品,请将 GET 请求发送到 http://localhost:port/odata/Products(1),其中 "1" 是产品 ID。

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
Date: Mon, 23 Sep 2013 23:04:29 GMT
Content-Length: 140

{
  "odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element","ID":1,
      "Name":"Hat","Price":"15.00","Category":"Apparel"
}

OData 序列化格式

OData 支持多种序列化格式:

  • Atom Pub (XML)
  • JSON "light" (在 OData v3 中引入)
  • JSON "verbose" (OData v2)

默认情况下,Web API 使用 AtomPubJSON "light" 格式。

若要获取 AtomPub 格式,请将 Accept 标头设置为 "application/atom + xml"。 下面是响应正文示例:

<?xml version="1.0" encoding="utf-8"?>
<entry xml:base="http://localhost:60868/odata" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
  <id>http://localhost:60868/odata/Products(1)</id>
  <category term="ProductService.Models.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
  <link rel="edit" href="http://localhost:60868/odata/Products(1)" />
  <link rel="self" href="http://localhost:60868/odata/Products(1)" />
  <title />
  <updated>2013-09-23T23:42:11Z</updated>
  <author>
    <name />
  </author>
  <content type="application/xml">
    <m:properties>
      <d:ID m:type="Edm.Int32">1</d:ID>
      <d:Name>Hat</d:Name>
      <d:Price m:type="Edm.Decimal">15.00</d:Price>
      <d:Category>Apparel</d:Category>
    </m:properties>
  </content>
</entry>

您可以看到 Atom 格式的一个明显缺点:它比 JSON 光更详细。 但是,如果你有一个了解 AtomPub 的客户端,则客户端可能更倾向于使用 JSON 格式。

下面是同一实体的 JSON 轻型版本:

{
  "odata.metadata":"http://localhost:60868/odata/$metadata#Products/@Element",
  "ID":1,
  "Name":"Hat",
  "Price":"15.00",
  "Category":"Apparel"
}

JSON 轻型格式是在 OData 协议版本3中引入的。 为实现向后兼容性,客户端可以请求较旧的 "详细" JSON 格式。 若要请求详细的 JSON,请将 Accept 标头设置为 application/json;odata=verbose。 下面是详细版本:

{
  "d":{
    "__metadata":{
      "id":"http://localhost:18285/odata/Products(1)",
      "uri":"http://localhost:18285/odata/Products(1)",
      "type":"ProductService.Models.Product"
    },"ID":1,"Name":"Hat","Price":"15.00","Category":"Apparel"
  }
}

此格式将更多的元数据传递到响应正文中,这可能会在整个会话中产生相当大的开销。 此外,它还通过将对象包装在名为 "d" 的属性中来添加间接级别。

后续步骤