[水煮 ASP.NET Web API2 方法论](12-4)OData 支持的 Function 和 Action

问题

在 Web API 中使用 OData Function 和 Action。

 

解决方案

可以通过 ODataModelBuilder,使用 OData 构建 ASP.NET Web API, EntityCollectionConfiguration,EnityTypeConfiguration 类中提供的一系列 Function 和 Action 来自定义 Function 和 Action。

当我们都建自己的 ODataModelBuilder 的时候,可以指定 Function 或 Action 名称并定义他们的输入参数。如清单12-12 所示。

清单 12-12. 

 1             var ODataBuilder = new ODataConventionModelBuilder();
 2 
 3             ODataBuilder.EntitySet<Player>("Players");
 4 
 5             var player = ODataBuilder.EntityType<Player>();
 6 
 7             // Function – 读取数据
 8 
 9             player.Function("PercentageOfAllGoals").Returns<double>();
10 
11             // Action – 请求操作
12 
13             player.Action("TradePlayer").Parameter<string>("NewTeam");

Controller Action 和 OData Function/Action 之间是通过命名的约定建立关联,因此,我们需要在 OData 的 controller 中添加合适的 Action。

 

工作原理

ASP.NET WEB API 从 2.2 版本开始支持 OData,而且,已经成为 OData 3.0 规范的一部分。另一方面,在之前 Web API 中 OData 的 Action 也是可以使用的。

我们是可以以 Web API Action 的形式定义 OData Function/Action 同时暴露给客户端访问。

使用 Action 或 Function 的主要优势是,我们可以将查询的责任转交给服务器,尤其是复杂查询的时候,可以减轻客户端的不必要的麻烦。

OData 的 Action 和 Function 是有点不一样的;他们都是在规范中被定义的“一组可以被执或可以作为服务或可以作为资源的操作的扩展”。主要的不同是

  • Function:可以能没有什么结果,但是必须有返回值
  • Action:可以对服务器产生影响,但是不能有返回值
  • 另外,Function 可以在 $filter 中被调用

在实现了 OData 的 ASP.NET WEB API 中,Action 和 Function 是连同 OData 约定一起被定义,他们是通过 ODataConventionModelBuilder 的实例定义。WEB API OData 的构建支持三种类型(级别)的操作:

  • 服务 Action/Function:ODataModelBuilder 直接定义
  • 集合 Action/Function:EntityCollectionConfiguration 直接定义
  • 实体 Action/Function:EntityTypeConfiguration直接定义

 

代码演示

 

如清单 12-13 所示,一个简单的数据集合,为了演示的方面,在 Controller 中通过内存进行数据的操作,还有一个 Player 的 DTO 的类。

我们就使用这些代码模拟 OData 的三种类型:服务,集合,实体绑定。演示中主要关注在 Function 上,但是, Action 的定义和使用也是几乎一样的。也就是说,在所有使用 Function 声明的方法的地方,都换成 Action 声明的方法是没有毛病的。

 

清单 12-13. 内存数据和实体模型

 1     public class Player
 2     {
 3         public int Id { get; set; }
 4 
 5         public string Name { get; set; }
 6 
 7         public string Team { get; set; }
 8 
 9         public SkaterStat Stats { get; set; }
10     }
11 
12     public class SkaterStat
13     {
14         public int Goals { get; set; }
15 
16         public int Assists { get; set; }
17 
18         public int GamesPlayed { get; set; }
19     }
20 
21     public class PlayersController : ODataController
22     {
23         private static List<Player> _players = new List<Player>
24         {
25             new Player
26             {
27                 Id = 1,
28                 Name = "Filip",
29                 Team = "Whales",
30                 Stats = new SkaterStat
31                 {
32                     GamesPlayed = 82,
33                     Goals = 37,
34                     Assists = 43
35                 }
36             },
37             new Player
38             {
39                 Id = 2,
40                 Name = "Felix",
41                 Team = "Whales",
42                 Stats = new SkaterStat
43                 {
44                     GamesPlayed = 80,
45                     Goals = 30,
46                     Assists = 31
47                 }
48             new Player
49 {
50     Id        = 3,
51                 Name = "Luiz",
52                 Team = "Dolphins",
53                 Stats = new SkaterStat
54                 {
55                     GamesPlayed = 78,
56                     Goals = 20,
57                     Assists = 30
58                 }
59             },
60             new Player
61             {
62                 Id = 4,
63                 Name = "Terry",
64                 Team = "Dolphins",
65                 Stats = new SkaterStat
66                 {
67                     GamesPlayed = 58,
68                     Goals = 19,
69                     Assists = 30
70                 }
71             }
72         };
73     }

前面提到的 Function 方法,来自于 ODataModelBuilder;EntityCollectionConfiguration,EntityTypeConfiguration,都返回一个 FunctionConfiguration 的实例,我们就是用它来配置我们的 Function,例如,在 $filter 中是否支持 Function,接收什么样的参数,应该返回什么。例如,这个演示的 Startup 类中定义了 ODataModelBuilder的 三个 OData  Function 类型和一个实体类型,如清单 12-14 所示。

 

清单 12-14 OData Function 服务、集合、实体

 1     public class Startup
 2     {
 3 
 4         public void Configuration(IAppBuilder builder)
 5         {
 6 
 7             var ODataBuilder = new ODataConventionModelBuilder();
 8 
 9             ODataBuilder.EntitySet<Player>("Players");
10 
11             var player = ODataBuilder.EntityType<Player>();
12 
13             /* 集合 Function */
14 
15             player.Collection.Function("TopPpg").ReturnsCollection<Player>();
16 
17             /* 实体 Function */
18 
19             player.Function("PercentageOfAllGoals").Returns<double>();
20 
21             /* 服务 Function */
22 
23             var serviceFunc = ODataBuilder.Function("TotalTeamPoints");
24 
25             serviceFunc.Returns<int>().Parameter<string>("team");
26 
27             serviceFunc.IncludeInServiceDocument = true;
28 
29             var edm = ODataBuilder.GetEdmModel();
30 
31             var config = new HttpConfiguration();
32 
33             config.MapODataServiceRoute("Default OData", "OData", edm);
34 
35             builder.UseWebApi(config);
36 
37         }
38 
39     }

TopPpg 是一个集合 Function,他将返回每场比赛最高分(得分+助攻)比例 player 的集合。PercentageOfAllGoals 是一个实体 Function,返回每场比赛给定参赛者相对所有得分的分数比例。这个 Function 需要客户端传一个 key(player ID),但是,需要注意的是,这个 key 是实体对象的 Id,不需要在 Function 中特殊指明。最后,TotalTeamPoints 是无限制的服务 Function,也就是说,不是特指某一个 player,而是传入一个队名最为参数,同时返回整个队内所有队员分数(得分+助攻)的总和。另外,TotalTeamPoints 也会包含在文档服务中,/OData/$metadata ,作为 Function 入口。

这些 Function 在 Action 中都是使用的 LINQ 表达式。无限制服务的 Function 使用了 ODataRoute 属性,因为默认的 EMD 驱动路由约定不能完成整个功能。

 

12-15/ 使用 OData Function 来暴露 Controller 的 Action

 1         [HttpGet]
 2         public IEnumerable<Player> TopPpg()
 3         {
 4             var result = _players.OrderByDescending(x => (double)(x.Stats.Goals + x.Stats.Assists) / (double)x.Stats.GamesPlayed).Take(3);
 5             return (result);
 6         }
 7 
 8 
 9         [HttpGet]
10         public IHttpActionResult PercentageOfAllGoals(int key)
11         {
12             var player = _players.FirstOrDefault(x => x.Id == key);
13             if (player == null)
14                 return (NotFound());
15             var result = (double)player.Stats.Goals / (double)_players.Sum(x => x.Stats.Goals) * 100;
16             return (Ok(result));
17         }
18 
19 
20         [HttpGet]
21         [ODataRoute("TotalTeamPoints(team={team})")]
22         public int TotalTeamPoints([FromODataUri] string team)
23         {
24             var result = _players.Where(x => string.Equals(x.Team, team, StringComparison.
25                                      InvariantCultureIgnoreCase))
26                      .Sum(x => x.Stats.Goals + x.Stats.Assists);
27             return (result);
28         }

在这些地方,可以在 URI 中使用 Function 名称来调用他们。根据规范,调用 OData Function 的时候需要使用括号:

  • /OData/Players/Default.TopPpg()
  • /OData/Players(1)/Default.PercentageOfAllGoals()
  • /OData/TotalTeamPoints(team='Whales')

 关于 OData 在 ASP.NET WEB API 中的介绍就此告一段落,接下来,一段时间将介绍关于 Route 的东西。