支持 ASP.NET Web API 2 中的 OData 操作

作者:Mike Wasson

下载已完成项目

在 OData 中, 操作 是添加服务器端行为的一种方法,这些行为不容易定义为实体上的 CRUD 操作。 操作的一些用途包括:

  • 实现复杂事务。
  • 一次操作多个实体。
  • 仅允许对实体的某些属性进行更新。
  • 将信息发送到未在实体中定义的服务器。

本教程中使用的软件版本

  • Web API 2
  • OData 版本 3
  • Entity Framework 6

示例:为产品评分

在此示例中,我们希望让用户对产品进行评分,然后公开每个产品的平均评分。 在数据库中,我们将存储指向产品的分级列表。

下面是可用于在实体框架中表示评级的模型:

public class ProductRating
{
    public int ID { get; set; }

    [ForeignKey("Product")]
    public int ProductID { get; set; }
    public virtual Product Product { get; set; }  // Navigation property

    public int Rating { get; set; }
}

但我们不希望客户端将对象发布到 ProductRating “Ratings”集合。 直观地说,分级与 Products 集合相关联,客户端应只需发布评分值。

因此,我们定义了客户端可以在 Product 上调用的操作,而不是使用常规 CRUD 操作。 在 OData 术语中,操作 绑定到 Product 实体。

操作会对服务器产生副作用。 出于此原因,使用 HTTP POST 请求调用它们。 操作可以具有参数和返回类型,如服务元数据中所述。 客户端在请求正文中发送参数,服务器在响应正文中发送返回值。 若要调用“Rate Product”操作,客户端会将 POST 发送到如下所示的 URI:

http://localhost/odata/Products(1)/RateProduct

POST 请求中的数据只是产品分级:

{"Rating":2}

在实体数据模型中声明操作

在 Web API 配置中,将操作添加到实体数据模型 (EDM) :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        builder.EntitySet<Supplier>("Suppliers");
        builder.EntitySet<ProductRating>("Ratings");

        // New code: Add an action to the EDM, and define the parameter and return type.
        ActionConfiguration rateProduct = builder.Entity<Product>().Action("RateProduct");
        rateProduct.Parameter<int>("Rating");
        rateProduct.Returns<double>();

        config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
    }
}

此代码将“RateProduct”定义为可以对 Product 实体执行的操作。 它还声明操作采用名为“Rating”的 int 参数,并返回 int 值。

将操作添加到控制器

“RateProduct”操作绑定到 Product 实体。 若要实现操作,请将名为 RateProduct 的方法添加到 Products 控制器:

[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    int rating = (int)parameters["Rating"];

    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    product.Ratings.Add(new ProductRating() { Rating = rating });
    db.SaveChanges();

    double average = product.Ratings.Average(x => x.Rating);

    return Ok(average);
}

请注意,方法名称与 EDM 中操作的名称匹配。 方法有两个参数:

  • 密钥:要评分的产品的密钥。
  • parameters:操作参数值的字典。

如果使用默认路由约定,则 key 参数必须命名为“key”。 还必须包括 [FromOdataUri] 属性,如下所示。 此属性告知 Web API 在分析请求 URI 中的密钥时使用 OData 语法规则。

使用 parameters 字典获取操作参数:

if (!ModelState.IsValid)
{
    return BadRequest();
}
int rating = (int)parameters["Rating"];

如果客户端以正确的格式发送操作参数, 则 ModelState.IsValid 的值为 true。 在这种情况下,可以使用 ODataActionParameters 字典获取参数值。 在此示例中,操作 RateProduct 采用名为“Rating”的单个参数。

操作元数据

若要查看服务元数据,请将 GET 请求发送到 /odata/$metadata。 下面是声明 RateProduct 操作的元数据部分:

<FunctionImport Name="RateProduct" m:IsAlwaysBindable="true" IsBindable="true" ReturnType="Edm.Double">
  <Parameter Name="bindingParameter" Type="ProductService.Models.Product"/>
  <Parameter Name="Rating" Nullable="false" Type="Edm.Int32"/>
</FunctionImport>

FunctionImport 元素声明操作。 大多数字段都是不言而喻的,但有两个字段值得注意:

  • IsBindable 意味着可以在目标实体上调用操作,至少在某些时间。
  • IsAlwaysBindable 意味着始终可以在目标实体上调用操作。

不同之处在于,某些操作始终可供客户端使用,但其他操作可能取决于实体的状态。 例如,假设你定义了“购买”操作。 只能购买有库存的商品。 如果商品缺货,则客户端无法调用该操作。

定义 EDM 时, Action 方法会创建始终可绑定的操作:

builder.Entity<Product>().Action("RateProduct"); // Always bindable

在本主题后面部分,我将讨论不始终可绑定的操作 (也称为 暂时性 操作) 。

调用操作

现在,让我们看看客户端将如何调用此操作。 假设客户端希望为 ID = 4 的产品提供 2 的评级。 下面是对请求正文使用 JSON 格式的示例请求消息:

POST http://localhost/odata/Products(4)/RateProduct HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":2}

下面是响应消息:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 3.0
Date: Tue, 22 Oct 2013 19:04:00 GMT
Content-Length: 89

{
  "odata.metadata":"http://localhost:21900/odata/$metadata#Edm.Double","value":2.75
}

将操作绑定到实体集

在前面的示例中,操作绑定到单个实体:客户端对单个产品进行评分。 还可以将操作绑定到实体集合。 只需进行以下更改:

在 EDM 中,将操作添加到实体的 Collection 属性。

var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");

在控制器方法中,省略 key 参数。

[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
    // ....
}

现在,客户端对 Products 实体集调用 操作:

http://localhost/odata/Products/RateAllProducts

具有集合参数的操作

操作可以具有采用值集合的参数。 在 EDM 中,使用 CollectionParameter<T> 声明 参数。

rateAllProducts.CollectionParameter<int>("Ratings");

这会声明一个名为“Ratings”的参数,该参数采用 int 值的集合。 在控制器方法中,仍从 ODataActionParameters 对象获取参数值,但现在该值是 ICollection<int> 值:

[HttpPost]
public void RateAllProducts(ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

    var ratings = parameters["Ratings"] as ICollection<int>; 

    // ...
}

暂时性操作

在“RateProduct”示例中,用户始终可以对产品进行评分,因此操作始终可用。 但某些操作取决于实体的状态。 例如,在视频租赁服务中,“签出”操作并非始终可用。 (这取决于该视频的副本是否可用。) 这种类型的操作称为 暂时性 操作。

在服务元数据中,暂时性操作的 IsAlwaysBindable 等于 false。 这实际上是默认值,因此元数据将如下所示:

<FunctionImport Name="CheckOut" IsBindable="true">
    <Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>

原因如下:如果操作是暂时性的,则服务器需要告知客户端操作何时可用。 它通过在实体中包含操作的链接来执行此操作。 下面是 Movie 实体的示例:

{
  "odata.metadata":"http://localhost:17916/odata/$metadata#Movies/@Element",
  "#CheckOut":{ "target":"http://localhost:17916/odata/Movies(1)/CheckOut" },
  "ID":1,"Title":"Sudden Danger 3","Year":2012,"Genre":"Action"
}

“#CheckOut”属性包含指向 CheckOut 操作的链接。 如果该操作不可用,服务器将省略该链接。

若要在 EDM 中声明暂时性操作,请调用 TransientAction 方法:

var checkoutAction = builder.Entity<Movie>().TransientAction("CheckOut");

此外,必须提供返回给定实体的操作链接的函数。 通过调用 HasActionLink 设置此函数。 可以将 函数编写为 lambda 表达式:

checkoutAction.HasActionLink(ctx =>
{
    var movie = ctx.EntityInstance as Movie;
    if (movie.IsAvailable) {
        return new Uri(ctx.Url.ODataLink(
            new EntitySetPathSegment(ctx.EntitySet), 
            new KeyValuePathSegment(movie.ID.ToString()),
            new ActionPathSegment(checkoutAction.Name)));
    }
    else
    {
        return null;
    }
}, followsConventions: true);

如果该操作可用,则 lambda 表达式将返回指向该操作的链接。 OData 序列化程序在序列化实体时包括此链接。 当操作不可用时,函数将 null返回 。

其他资源