如何在同一OData EDM模型中注册同名不同命名空间的控制器及相关路由问题
目标
本文旨在深入了解如何在ASP.NET Core之上采用模块化(第三方插件)方式开发API。为降低插件开发门槛,应尽可能依赖约定,并实现高价值交付。OData的查询能力及其标准化特性相比REST仍具备显著优势,投入产出比极高。
问题
OData功能强大,但现有文档不够完善,因此开发者对其限制和能力缺乏清晰认知。由此引发了关于如何解决第三方模块中控制器端点冲突导致的路由问题的疑问,具体如下:
- Q1:单个OData EDM模型能否区分两个同名但不同命名空间的控制器?
- Q2:能否为
ODataController注册与控制器名称不同的路由(例如路由Foo指向BarController,而按约定应指向FooController)且不破坏默认功能?(例如:$count功能失效)- Q2b:即便我们接管传入Uri解析为OData路径组件的逻辑以及查找相关控制器的逻辑,是否可行?
- Q3:如果无法实现,加载插件更实用的方式是否是让每个插件注册自己的EDM模型?
- Q4:当第二个插件/EDM模型的控制器暴露的模型包含引用基础(或其他依赖插件)EDM模型中控制器所暴露模型的属性时,使用多个EDM模型是否可行?
- 示例:基础EDM暴露
Person和Address,第二个插件EDM暴露Customer,其属性引用基础EDM提供的Person和Address类型? - (我猜测可行,但无法完全确定……有人能发现其中的问题吗?)
- 示例:基础EDM暴露
- Q5:如何动态添加新的EDM模型并重置路由?
- 例如,若上传一个作为插件的NuGet包,其中包含描述其模型和控制器的EDM模型……这一切发生在
after.Run()调用之后,如何让系统重新识别有效路由?
- 例如,若上传一个作为插件的NuGet包,其中包含描述其模型和控制器的EDM模型……这一切发生在
- Q6:为何
$count仅在通过约定注册的ODataController上可用,而在通过RouteAttribute注册的控制器上失效?
背景
我找到的最新文档是一篇关于ASP.NET Core OData 8.0 RC属性路由的博客文章,但这只是一篇博客文章,仍有大量内容缺失,无法理解所有组件及其协作方式。
当前进展
我列出以下笔记,帮助他人更好地了解我的尝试,以便有人能发现我明显的错误。如果需要可运行的代码,我已上传我的研究代码到GitHub仓库。
环境配置
OData的默认场景是注册EDM模型,使其控制器路由与控制器名称前缀一致。
示例:
static string ODataPrefixWithSlash = "api/odata/v{version}/" class SomeModelController : ODataController {...} //使用匹配控制器名称前缀的约定在EDM模型中注册: var builder = new ODataConventionModelBuilder(); //构建完整模型: builder.EntitySet<SomeModel>("SomeModel"); //可被识别 builder.EntitySet<SomeModel>("Renamed"); //若无额外操作会导致404,因为路由字符串与控制器前缀不匹配 var edmModelA = builder.Build(); //之后将模型注册为OData路由信息源: var mvcBuilder = builder.Services .AddControllers() .AddOData( opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) //添加模块/插件A路由: .AddRouteComponents(AppAPIConstants.ODataPrefixWithSlash,edmModelA);
若已启用
builder.Services.AddSwaggerGen(); app.UseODataRouteDebug();
可导航至~odata查看控制器端点列表:
~api/odata/v{version}/SomeModel ~api/odata/v{version}/SomeModel/$count <- 目前可见(后续会失效……原因何在?!)
控制器选择流程
有一篇文章介绍了OData v8 RC中路由的更新,其依赖RouteAttribute。经过多次尝试后发现,可使用不同名称,但需满足:
- 路由必须以注册EDM模型时使用的相同前缀开头(例如:
api/odata/v{version}/)
[ODataAttributeRouting] // 部分可用 // 因为路由以注册模型时使用的相同前缀开头 // 使其被识别为OData控制器 // a) 在~/$odata中被列为OData控制器(位于api/odata/v{version}下) // b) 表现为OData控制器(返回OData格式的JSON包装器) // c) 但缺少默认查询能力(不支持或不显示/$count) // 路由前缀与注册EDM模型时的约定一致 // 但$count功能失效! [Route(AppAPIConstants.ODataPrefixWithSlash + "Renamed5")] public class ValuesA5Controller : ODataController{ [EnableQuery(PageSize = 100)] [HttpGet("")] [HttpGet("Get")] public IActionResult Get() { return Ok(FakeDataBuilder.Get()); } }
上述代码允许在EDM模型中按如下方式注册ODataController:
builder.EntitySet<SomeModel>("Renamed5");
但仅部分可用:
- 在
$odata中被列为OData控制器,正常。 - 大部分表现为OData控制器,支持$select、$filter等OData命令。
- 但
$count功能无故失效:
~api/odata/v{version}/Renamed5 <- 可见,与之前的SomeModelController一致 ~api/odata/v{version}/Renamed5/$count <- 不可见,尽管端点已标记[EnableQuery]。
可选方案?
方案A:添加更多路由信息
我猜测可为Get方法添加更多路由以启用$count:
[EnableQuery(PageSize = 100)] [HttpGet("")] [HttpGet("Get")] [HttpGet("$count")] // 真的需要这样吗? public IActionResult Get() {....}
当然希望避免添加此类代码作为临时解决方法,但即便可行,也会增加代码量和出错概率。我希望尽可能减少代码,依赖约定,同时不限制控制器名称。
方案B:控制器选择流程
如前所述,我不清楚OData框架如何自动将"SomeModel"匹配到"SomeModelController"。我在一篇文章中看到了AttributeRoutingConvention: IODataControllerActionConvention,或许可加以利用,但博客文章未展示其注册或替换方式,因此无法推进。
此外,遍历已注册服务时,未发现任何继承自IODataControllerActionConvention的组件,这是怎么回事?
我看到的组件有:
//尚未明确其用途 IODataQueryRequestParser IODataTemplateTranslator : Microsoft.AspNetCore.OData.Routing.Template.DefaultODataTemplateTranslator IODataPathTemplateParser : Microsoft.AspNetCore.OData.Routing.Parser.DefaultODataPathTemplateParser`
但缺乏相关文档说明其工作原理和用途。
下一步方向?
- 首先,感谢您抽出时间阅读这个冗长的问题!
- 其次,若您能指出我的思路误区,将不胜感激。
- 最后,若您能解答上述问题……那真是太好了!我寻找这些答案已有一段时间了!
内容的提问来源于stack exchange,提问作者Sky

