使用 ASP.NET Core OData 8.0 映射动态 odata 路由
Mapping dynamic odata routes with ASP.NET Core OData 8.0
我有一个应用程序,其中 EDM 数据类型是在应用程序运行时生成的(它们甚至可以在运行时更改)。松散地基于 OData DynamicEDMModelCreation Sample - 重构以使用新的端点路由。 EDM 模型在运行时动态生成,所有请求都转发到同一个控制器。
现在我想更新到最新的 ASP.NET Core OData 8.0 并且整个路由发生了变化,因此当前的解决方法不再有效。
我已经阅读了更新 Blog1Blog2 的两篇博文,似乎我不能再使用“旧”解决方法作为函数 MapODataRoute() 端点内现在不见了。内置路由约定的 none 似乎也适用于我的用例,因为所有这些都需要在调试时存在 EDM 模型。
也许我可以使用自定义 IODataControllerActionConvention。我试图通过将其添加到路由约定来激活约定,但似乎我仍然缺少如何激活它的部分。
services.TryAddEnumerable(
ServiceDescriptor.Transient<IODataControllerActionConvention, MyEntitySetRoutingConvention>());
这种方法是否有效?甚至可以在新的 odata 预览中激活动态模型吗?或者有人知道如何处理新 odata 8.0 的动态路由吗?
因此,经过 5 天的内部 OData 调试后,我设法让它工作了。以下是必要的步骤:
首先从您的 controller/configure 服务中删除所有 OData calls/attributes,这些服务可能会做奇怪的事情(ODataRoutingAttribute
或 AddOData()
)
使用您喜欢的路线创建一个简单的 asp.net 控制器并将其映射到端点
[ApiController]
[Route("odata/v{version}/{Path?}")]
public class HandleAllController : ControllerBase { ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
}
}
创建并注册您的 InputFormatWrapper 和 OutputFormatWrapper
public class ConfigureMvcOptionsFormatters : IConfigureOptions<MvcOptions>
{
private readonly IServiceProvider _services;
public ConfigureMvcOptionsFormatters(IServiceProvider services)
{
_services = services;
}
public void Configure(MvcOptions options)
{
options.InputFormatters.Insert(0, new ODataInputFormatWrapper(_services));
options.OutputFormatters.Insert(0, new OdataOutputFormatWrapper(_services));
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.ConfigureOptions<ConfigureMvcOptionsFormatters>();
...
}
public class ODataInputFormatWrapper : InputFormatter
{
private readonly IServiceProvider _serviceProvider;
private readonly ODataInputFormatter _oDataInputFormatter;
public ODataInputFormatWrapper(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
//JSON and default is first - see factory comments
_oDataInputFormatter = ODataInputFormatterFactory.Create().First();
}
public override bool CanRead(InputFormatterContext context)
{
if (!ODataWrapperHelper.IsRequestValid(context.HttpContext, _serviceProvider))
return false;
return _oDataInputFormatter.CanRead(context);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
return _oDataInputFormatter!.ReadRequestBodyAsync(context);
}
}
// The OutputFormatWrapper looks like the InputFormatWrapper
在 ODataWrapperHelper
中,您可以检查内容和 get/set 您的动态 edmModel。最后有必要设置这些ODataFeature()
...这并不漂亮,但它完成了动态工作...
public static bool IsRequestValid(HttpContext context, IServiceProvider serviceProvider)
{
//... Do stuff, get datasource
var edmModel = dataSource!.GetModel();
var oSegment = new EntitySetSegment(new EdmEntitySet(edmModel.EntityContainer, targetEntity, edmModel.SchemaElements.First(x => targetEntity == x.Name) as EdmEntityType));
context.ODataFeature().Services = serviceProvider.CreateScope().ServiceProvider;
context.ODataFeature().Model = edmModel;
context.ODataFeature().Path = new ODataPath(oSegment);
return true;
}
现在是丑陋的东西:我们仍然需要在 ConfigureServices(IServiceCollection services)
中注册一些 ODataService。我在那里添加了一个名为 AddCustomODataService(services)
的函数,您可以在那里自己注册 ~40 项服务或做一些时髦的反思...
所以也许如果 odata 团队的人读到这篇文章,请考虑打开 Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions
我创建了一个
public class CustomODataServiceContainerBuilder : IContainerBuilder
这是内部 Microsoft.AspNetCore.OData.Abstracts.DefaultContainerBuilder
的副本 我添加了函数:
public void AddServices(IServiceCollection services)
{
foreach (var service in Services)
{
services.Add(service);
}
}
和丑陋的AddCustomODataServices(IServiceCollection services)
private void AddCustomODataService(IServiceCollection services)
{
var builder = new CustomODataServiceContainerBuilder();
builder.AddDefaultODataServices();
//AddDefaultWebApiServices in ContainerBuilderExtensions is internal...
var addDefaultWebApiServices = typeof(ODataFeature).Assembly.GetTypes()
.First(x => x.FullName == "Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions")
.GetMethods(BindingFlags.Static|BindingFlags.Public)
.First(x => x.Name == "AddDefaultWebApiServices");
addDefaultWebApiServices.Invoke(null, new object?[]{builder});
builder.AddServices(services);
}
现在控制器应该再次工作(使用 odataQueryContext 和序列化)- 示例:
[HttpGet]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
//... get model and entitytype
var queryContext = new ODataQueryContext(model, entityType, null);
var queryOptions = new ODataQueryOptions(queryContext, Request);
return (Collection<IEdmEntityObject>)myCollection;
}
[HttpPost]
public Task<IActionResult> Post([FromBody] IEdmEntityObject entityDataObject, CancellationToken cancellationToken)
{
//Do something with IEdmEntityObject
return Ok()
}
这里有一个动态路由和动态模型的例子:
https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataDynamicModel/
请参阅 MyODataRoutingApplicationModelProvider
和 MyODataRoutingMatcherPolicy
,它们会将自定义 IEdmModel 传递给控制器。
HandleAllController
可以动态处理不同的类型和 edm 模型。
我有一个应用程序,其中 EDM 数据类型是在应用程序运行时生成的(它们甚至可以在运行时更改)。松散地基于 OData DynamicEDMModelCreation Sample - 重构以使用新的端点路由。 EDM 模型在运行时动态生成,所有请求都转发到同一个控制器。
现在我想更新到最新的 ASP.NET Core OData 8.0 并且整个路由发生了变化,因此当前的解决方法不再有效。
我已经阅读了更新 Blog1Blog2 的两篇博文,似乎我不能再使用“旧”解决方法作为函数 MapODataRoute() 端点内现在不见了。内置路由约定的 none 似乎也适用于我的用例,因为所有这些都需要在调试时存在 EDM 模型。
也许我可以使用自定义 IODataControllerActionConvention。我试图通过将其添加到路由约定来激活约定,但似乎我仍然缺少如何激活它的部分。
services.TryAddEnumerable(
ServiceDescriptor.Transient<IODataControllerActionConvention, MyEntitySetRoutingConvention>());
这种方法是否有效?甚至可以在新的 odata 预览中激活动态模型吗?或者有人知道如何处理新 odata 8.0 的动态路由吗?
因此,经过 5 天的内部 OData 调试后,我设法让它工作了。以下是必要的步骤:
首先从您的 controller/configure 服务中删除所有 OData calls/attributes,这些服务可能会做奇怪的事情(ODataRoutingAttribute
或 AddOData()
)
使用您喜欢的路线创建一个简单的 asp.net 控制器并将其映射到端点
[ApiController]
[Route("odata/v{version}/{Path?}")]
public class HandleAllController : ControllerBase { ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
}
}
创建并注册您的 InputFormatWrapper 和 OutputFormatWrapper
public class ConfigureMvcOptionsFormatters : IConfigureOptions<MvcOptions>
{
private readonly IServiceProvider _services;
public ConfigureMvcOptionsFormatters(IServiceProvider services)
{
_services = services;
}
public void Configure(MvcOptions options)
{
options.InputFormatters.Insert(0, new ODataInputFormatWrapper(_services));
options.OutputFormatters.Insert(0, new OdataOutputFormatWrapper(_services));
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.ConfigureOptions<ConfigureMvcOptionsFormatters>();
...
}
public class ODataInputFormatWrapper : InputFormatter
{
private readonly IServiceProvider _serviceProvider;
private readonly ODataInputFormatter _oDataInputFormatter;
public ODataInputFormatWrapper(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
//JSON and default is first - see factory comments
_oDataInputFormatter = ODataInputFormatterFactory.Create().First();
}
public override bool CanRead(InputFormatterContext context)
{
if (!ODataWrapperHelper.IsRequestValid(context.HttpContext, _serviceProvider))
return false;
return _oDataInputFormatter.CanRead(context);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
return _oDataInputFormatter!.ReadRequestBodyAsync(context);
}
}
// The OutputFormatWrapper looks like the InputFormatWrapper
在 ODataWrapperHelper
中,您可以检查内容和 get/set 您的动态 edmModel。最后有必要设置这些ODataFeature()
...这并不漂亮,但它完成了动态工作...
public static bool IsRequestValid(HttpContext context, IServiceProvider serviceProvider)
{
//... Do stuff, get datasource
var edmModel = dataSource!.GetModel();
var oSegment = new EntitySetSegment(new EdmEntitySet(edmModel.EntityContainer, targetEntity, edmModel.SchemaElements.First(x => targetEntity == x.Name) as EdmEntityType));
context.ODataFeature().Services = serviceProvider.CreateScope().ServiceProvider;
context.ODataFeature().Model = edmModel;
context.ODataFeature().Path = new ODataPath(oSegment);
return true;
}
现在是丑陋的东西:我们仍然需要在 ConfigureServices(IServiceCollection services)
中注册一些 ODataService。我在那里添加了一个名为 AddCustomODataService(services)
的函数,您可以在那里自己注册 ~40 项服务或做一些时髦的反思...
所以也许如果 odata 团队的人读到这篇文章,请考虑打开 Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions
我创建了一个
public class CustomODataServiceContainerBuilder : IContainerBuilder
这是内部 Microsoft.AspNetCore.OData.Abstracts.DefaultContainerBuilder
的副本 我添加了函数:
public void AddServices(IServiceCollection services)
{
foreach (var service in Services)
{
services.Add(service);
}
}
和丑陋的AddCustomODataServices(IServiceCollection services)
private void AddCustomODataService(IServiceCollection services)
{
var builder = new CustomODataServiceContainerBuilder();
builder.AddDefaultODataServices();
//AddDefaultWebApiServices in ContainerBuilderExtensions is internal...
var addDefaultWebApiServices = typeof(ODataFeature).Assembly.GetTypes()
.First(x => x.FullName == "Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions")
.GetMethods(BindingFlags.Static|BindingFlags.Public)
.First(x => x.Name == "AddDefaultWebApiServices");
addDefaultWebApiServices.Invoke(null, new object?[]{builder});
builder.AddServices(services);
}
现在控制器应该再次工作(使用 odataQueryContext 和序列化)- 示例:
[HttpGet]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
//... get model and entitytype
var queryContext = new ODataQueryContext(model, entityType, null);
var queryOptions = new ODataQueryOptions(queryContext, Request);
return (Collection<IEdmEntityObject>)myCollection;
}
[HttpPost]
public Task<IActionResult> Post([FromBody] IEdmEntityObject entityDataObject, CancellationToken cancellationToken)
{
//Do something with IEdmEntityObject
return Ok()
}
这里有一个动态路由和动态模型的例子:
https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataDynamicModel/
请参阅 MyODataRoutingApplicationModelProvider
和 MyODataRoutingMatcherPolicy
,它们会将自定义 IEdmModel 传递给控制器。
HandleAllController
可以动态处理不同的类型和 edm 模型。