[水煮 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 的东西。