在 ASP.NET Web API 2 OData 中使用 $select、$expand 和 $value

作者:Mike Wasson

适用于 ASP.NET 4.x 的 OData Web API 2 中$expand、$select和$value选项的概述和代码示例。 这些选项允许客户端控制它从服务器返回的表示形式。

  • $expand 会导致相关实体内联包含在响应中。
  • $select 选择要包含在响应中的属性子集。
  • $value 获取属性的原始值。

示例架构

在本文中,我将使用定义三个实体的 OData 服务:Product、Supplier 和 Category。 每个产品都有一个类别和一个供应商。

显示 O Data 服务的示例架构的示意图,其中将“产品”、“供应商”和“类别”定义为其实体。

下面是定义实体模型的 C# 类:

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

请注意, Product 类定义 和 CategorySupplier导航属性。 类 Category 为每个类别中的产品定义导航属性。

若要为此架构创建 OData 终结点,请使用Visual Studio 2013基架,如在 ASP.NET Web API 中创建 OData 终结点中所述。 为“产品”、“类别”和“供应商”添加单独的控制器。

启用$expand和$select

在 Visual Studio 2013 中,Web API OData 基架创建自动支持$expand和$select的控制器。 下面是在控制器中支持$expand和$select的要求,以供参考。

对于集合,控制器的 Get 方法必须返回 IQueryable

[Queryable]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

对于单个实体,返回 SingleResult<T>,其中 T 是包含零个或一个实体 的 IQueryable

[Queryable]
public SingleResult<Category> GetCategory([FromODataUri] int key)
{
    return SingleResult.Create(db.Categories.Where(c => c.ID == key));
}

此外,使用 [Queryable] 属性修饰Get方法,如前面的代码片段所示。 或者,在启动时对 HttpConfiguration 对象调用 EnableQuerySupport。 (有关详细信息,请参阅 启用 OData 查询选项。)

使用 $expand

查询 OData 实体或集合时,默认响应不包括相关实体。 例如,下面是 Categories 实体集的默认响应:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {"ID":1,"Name":"Apparel"},
    {"ID":2,"Name":"Toys"}
  ]
}

如你所看到的,响应不包含任何产品,即使 Category 实体具有“产品”导航链接。 但是,客户端可以使用$expand来获取每个类别的产品列表。 $expand选项位于请求的查询字符串中:

GET http://localhost/odata/Categories?$expand=Products

现在,服务器将包括每个类别的产品,与类别内联。 下面是响应有效负载:

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

请注意,“value”数组中的每个条目都包含一个 Products 列表。

$expand选项采用逗号分隔的导航属性列表进行展开。 以下请求扩展了产品的类别和供应商。

GET http://localhost/odata/Products(1)?$expand=Category,Supplier

下面是响应正文:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products/@Element",
  "Category": {"ID":1,"Name":"Apparel"},
  "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
  "ID":1,
  "Name":"Hat",
  "Price":"15.00",
  "CategoryId":1,
  "SupplierId":"CTSO"
}

可以展开多个级别的导航属性。 以下示例包括一个类别的所有产品,以及每个产品的供应商。

GET http://localhost/odata/Categories(1)?$expand=Products/Supplier

下面是响应正文:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories/@Element",
  "Products":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"
    },{
      "Supplier":{
        "Key":"FBRK","Name":"Fabrikam, Inc."
      },"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"
    }
  ],"ID":1,"Name":"Apparel"
}

默认情况下,Web API 将最大扩展深度限制为 2。 这可以防止客户端发送复杂的请求,例如 $expand=Orders/OrderDetails/Product/Supplier/Region,这可能会降低查询和创建大型响应的效率。 若要替代默认值,请在 [Queryable] 属性上设置 MaxExpansionDepth 属性。

[Queryable(MaxExpansionDepth=4)]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

有关 $expand 选项的详细信息,请参阅官方 OData 文档中的 Expand System Query Option ($expand)

使用 $select

$select 选项指定要包含在响应正文中的属性子集。 例如,若要仅获取每个产品的名称和价格,请使用以下查询:

GET http://localhost/odata/Products?$select=Price,Name

下面是响应正文:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Price,Name",
  "value":[
    {"Price":"15.00","Name":"Hat"},
    {"Price":"12.00","Name":"Scarf"},
    {"Price":"5.00","Name":"Socks"},
    {"Price":"4.95","Name":"Yo-yo"},
    {"Price":"8.00","Name":"Puzzle"}
  ]
}

可以在同一查询中组合$select和$expand。 请确保在“$select”选项中包含展开的属性。 例如,以下请求获取产品名称和供应商。

GET http://localhost/odata/Products?$select=Name,Supplier&$expand=Supplier

下面是响应正文:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Name,Supplier",
  "value":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Hat"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Scarf"
    },
    {
      "Supplier":{"Key":"FBRK","Name":"Fabrikam, Inc."},
      "Name":"Socks"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Yo-yo"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Puzzle"
   }
  ]
}

还可以选择展开的属性中的属性。 以下请求展开“产品”并选择“类别名称”和“产品名称”。

GET http://localhost/odata/Categories?$expand=Products&$select=Name,Products/Name

下面是响应正文:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories&$select=Name,Products/Name",
  "value":[ 
    {
      "Products":[ {"Name":"Hat"},{"Name":"Scarf"},{"Name":"Socks"} ],
      "Name":"Apparel"
    },
    {
      "Products":[ {"Name":"Yo-yo"},{"Name":"Puzzle"} ],
      "Name":"Toys"
    }
  ]
}

有关$select选项的详细信息,请参阅官方 OData 文档中 ($select) 选择系统查询选项

获取实体 ($value) 的各个属性

OData 客户端可通过两种方式从实体获取单个属性。 客户端可以获取 OData 格式的值,也可以获取属性的原始值。

以下请求获取 OData 格式的属性。

GET http://localhost/odata/Products(1)/Name

下面是 JSON 格式的示例响应:

HTTP/1.1 200 OK
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 90

{
  "odata.metadata":"http://localhost:14239/odata/$metadata#Edm.String",
  "value":"Hat"
}

若要获取 属性的原始值,请将$value追加到 URI:

GET http://localhost/odata/Products(1)/Name/$value

下面是响应。 请注意,内容类型为“text/plain”,而不是 JSON。

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Hat

若要在 OData 控制器中支持这些查询,请添加一个名为 GetProperty的方法,其中 Property 是属性的名称。 例如,用于获取 Name 属性的方法将命名为 GetName。 方法应返回该属性的值:

public async Task<IHttpActionResult> GetName(int key)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product.Name);
}