支持 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
返回 。