ASP.NET Web API 2 中支持 OData 操作Supporting OData Actions in ASP.NET Web API 2

作者: Mike Wassonby Mike Wasson

下载完成的项目Download Completed Project

在 OData 中,操作是一种添加服务器端行为的方法,这些行为不容易定义为对实体的 CRUD 操作。In OData, actions are a way to add server-side behaviors that are not easily defined as CRUD operations on entities. 操作的一些用途包括:Some uses for actions include:

  • 实现复杂事务。Implementing complex transactions.
  • 一次操作多个实体。Manipulating several entities at once.
  • 仅允许更新实体的某些属性。Allowing updates only to certain properties of an entity.
  • 将信息发送到实体中未定义的服务器。Sending information to the server that is not defined in an entity.

本教程中使用的软件版本Software versions used in the tutorial

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

示例:对产品进行评级Example: Rating a Product

在此示例中,我们希望让用户对产品进行评级,并为每个产品公开平均评分。In this example, we want to let users rate products, and then expose the average ratings for each product. 在数据库中,我们将存储评级列表,并将其键控到产品。On the database, we will store a list of ratings, keyed to products.

下面是可以用来表示实体框架中的分级的模型:Here is the model we might use to represent the ratings in Entity Framework:

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 对象发布到 "分级" 集合中。But we don't want clients to POST a ProductRating object to a "Ratings" collection. 从直观上说,评级与 Products 集合相关联,并且客户端应只需要发布评级值。Intuitively, the rating is associated with the Products collection, and the client should only need to post the rating value.

因此,我们定义了客户端可以对产品调用的操作,而不是使用一般的 CRUD 操作。Therefore, instead of using the normal CRUD operations, we define an action that a client can invoke on a Product. 在 OData 术语中,此操作将绑定到产品实体。In OData terminology, the action is bound to Product entities.

操作在服务器上有副作用。Actions have side-effects on the server. 出于此原因,将使用 HTTP POST 请求来调用它们。For this reason, they are invoked using HTTP POST requests. 操作可以具有参数和返回类型,在服务元数据中进行了介绍。Actions can have parameters and return types, which are described in the service metadata. 客户端会在请求正文中发送参数,服务器将在响应正文中发送返回值。The client sends the parameters in the request body, and the server sends the return value in the response body. 若要调用 "Rate Product" 操作,客户端需要向 URI 发送 POST,如下所示:To invoke the "Rate Product" action, the client sends a POST to a URI like the following:

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

POST 请求中的数据只是产品分级:The data in the POST request is simply the product rating:

{"Rating":2}

在实体数据模型中声明操作Declare the Action in the Entity Data Model

在 Web API 配置中,将操作添加到实体数据模型(EDM):In your Web API configuration, add the action to the entity data model (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" 定义为可对产品实体执行的操作。This code defines "RateProduct" as an action that can be performed on Product entities. 它还声明操作采用名为 "分级" 的int参数,并返回一个整数值。It also declares that the action takes an int parameter named "Rating", and returns an int value.

将操作添加到控制器Add the Action to the Controller

"RateProduct" 操作绑定到产品实体。The "RateProduct" action is bound to Product entities. 若要实现此操作,请向 Products 控制器添加一个名为 RateProduct 的方法:To implement the action, add a method named RateProduct to the Products controller:

[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 中操作的名称相匹配。Notice that the method name matches the name of the action in the EDM. 此方法具有两个参数:The method has two parameters:

  • key:要进行评级的产品的键。key: The key for the product to rate.
  • parameters:操作参数值的字典。parameters: A dictionary of action parameter values.

如果使用默认路由约定,则必须将密钥参数命名为 "key"。If you are using the default routing conventions, the key parameter must be named "key". 还必须包含 [FromOdataUri] 属性,如下所示。It is also important to include the [FromOdataUri] attribute, as shown. 此属性告知 Web API 在分析来自请求 URI 的密钥时使用 OData 语法规则。This attribute tells Web API to use OData syntax rules when it parses the key from the request URI.

使用parameters字典获取操作参数:Use the parameters dictionary to get the action parameters:

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

如果客户端发送正确格式的操作参数,则ModelState的值为 true。If the client sends the action parameters in the correct format, the value of ModelState.IsValid is true. 在这种情况下,可以使用ODataActionParameters字典获取参数值。In that case, you can use the ODataActionParameters dictionary to get the parameter values. 在此示例中,RateProduct 操作采用一个名为 "分级" 的参数。In this example, the RateProduct action takes a single parameter named "Rating".

操作元数据Action Metadata

若要查看服务元数据,请将 GET 请求发送到/odata/$metadata。To view the service metadata, send a GET request to /odata/$metadata. 下面是声明 RateProduct 操作的元数据部分:Here is the portion of the metadata that declares the RateProduct action:

<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元素声明操作。The FunctionImport element declares the action. 大多数字段都一目了然,但有两个值得注意的事项:Most of the fields are self-explanatory, but two are worth noting:

  • IsBindable是指在目标实体上至少可以调用此操作。IsBindable means the action can be invoked on the target entity, at least some of the time.
  • IsAlwaysBindable表示操作始终可以在目标实体上调用。IsAlwaysBindable means the action can always be invoked on the target entity.

不同之处在于一些操作对客户端始终可用,但其他操作可能取决于实体的状态。The difference is that some actions are always available to clients, but other actions might depend on the state of the entity. 例如,假设您定义了 "购买" 操作。For example, suppose you define a "Purchase" action. 只能购买库存项。You can only purchase an item that is in stock. 如果项脱销,客户端将无法调用该操作。If the item is out of stock, a client cannot invoke that action.

定义 EDM 时,操作方法会创建一个始终可绑定的操作:When you define the EDM, the Action method creates an always-bindable action:

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

我将在本主题的后面部分讨论非始终可绑定的操作(也称为暂时性操作)。I'll talk about not-always-bindable actions (also called transient actions) later in this topic.

调用操作Invoking the Action

现在让我们看一下客户端将如何调用此操作。Now let's see how a client would invoke this action. 假设客户端要向 ID 为4的产品提供2的评级。Suppose the client wants to give a rating of 2 to the product with ID = 4. 下面是使用请求正文的 JSON 格式的示例请求消息:Here is an example request message, using JSON format for the request body:

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

{"Rating":2}

下面是响应消息:Here is the response message:

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
}

将操作绑定到实体集Binding an Action to an Entity Set

在上面的示例中,操作绑定到单个实体:客户端对单个产品进行评级。In the previous example, the action is bound to a single entity: The client rates a single product. 你还可以将操作绑定到实体的集合。You can also bind an action to a collection of entities. 只需进行以下更改:Just make the following changes:

在 EDM 中,将操作添加到实体的集合属性。In the EDM, add the action to the entity's Collection property.

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

在控制器方法中,省略key参数。In the controller method, omit the key parameter.

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

现在,客户端将调用 Products 实体集上的操作:Now the client invokes the action on the Products entity set:

http://localhost/odata/Products/RateAllProducts

具有集合参数的操作Actions with Collection Parameters

操作可以具有采用值集合的参数。Actions can have parameters that take a collection of values. 在 EDM 中,使用CollectionParameter<t> 声明参数。In the EDM, use CollectionParameter<T> to declare the parameter.

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

这将声明一个名为 "分级" 的参数,该参数采用int值的集合。This declares a parameter named "Ratings" that takes a collection of int values. 在控制器方法中,仍从ODataActionParameters对象获取参数值,但现在值为ICollection<int> 值:In the controller method, you still get the parameter value from the ODataActionParameters object, but now the value is an ICollection<int> value:

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

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

    // ...
}

暂时性操作Transient Actions

在 "RateProduct" 示例中,用户始终可以对产品进行分级,因此该操作始终可用。In the "RateProduct" example, users can always rate a product, so the action is always available. 但某些操作依赖于实体的状态。But some actions depend on the state of the entity. 例如,在视频租赁服务中,"结帐" 操作并非始终可用。For example, in a video rental service, the "CheckOut" action is not always available. (这取决于该视频的副本是否可用。)这种类型的操作称为暂时性操作。(It depends whether a copy of that video is available.) This type of action is called a transient action.

在服务元数据中,暂时性操作的IsAlwaysBindable等于 false。In the service metadata, a transient action has IsAlwaysBindable equal to false. 这实际上是默认值,因此元数据将如下所示:That's actually the default value, so the metadata will look like this:

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

这就是这一问题的原因:如果操作是暂时性的,则服务器需要在操作可用时通知客户端。Here's why this matters: If an action is transient, the server needs to tell the client when the action is available. 它通过在实体中包含指向操作的链接来完成此操作。It does this by including a link to the action in the entity. 下面是电影实体的示例:Here is an example for a Movie entity:

{
  "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" 属性包含指向签出操作的链接。The "#CheckOut" property contains a link to the CheckOut action. 如果此操作不可用,则服务器将忽略链接。If the action is not available, the server omits the link.

若要在 EDM 中声明暂时性操作,请调用TransientAction方法:To declare a transient action in the EDM, call the TransientAction method:

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

此外,还必须提供一个函数,该函数返回给定实体的操作链接。Also, you must provide a function that returns an action link for a given entity. 通过调用HasActionLink来设置此函数。Set this function by calling HasActionLink. 可以将函数编写为 lambda 表达式:You can write the function as a lambda expression:

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 表达式将返回指向操作的链接。If the action is available, the lambda expression returns a link to the action. OData 序列化程序在序列化实体时包含此链接。The OData serializer includes this link when it serializes the entity. 当操作不可用时,函数将返回 nullWhen the action is not available, the function returns null.

其他资源Additional Resources

OData 操作示例OData Actions Sample