So after 5 days of internal OData debugging I managed to get it to work. Here are the necessary steps:
First remove all OData calls/attributes from your controller/configure services which might do funky stuff (ODataRoutingAttribute or AddOData())
Create a simple asp.net controller with the route to your liking and map it in the endpoints
[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();
}
}
Create and register your InputFormatWrapper and 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
Within the ODataWrapperHelper you can check stuff and get/set your dynamic edmModel. It is necessary in the end to set these ODataFeature()... That's not beautiful but it gets the dynamic job done...
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;
}
Now to the ugly stuff: We still need to register some ODataService in ConfigureServices(IServiceCollection services). I added a function there called AddCustomODataService(services) and there you can either register ~40 services yourself or do some funky reflection...
So maybe if someone from the odata team reads this, please consider opening Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions
I created a
public class CustomODataServiceContainerBuilder : IContainerBuilder which is a copy of the internal Microsoft.AspNetCore.OData.Abstracts.DefaultContainerBuilder there I added the function:
public void AddServices(IServiceCollection services)
{
foreach (var service in Services)
{
services.Add(service);
}
}
and the ugly 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);
}
Now the controller should work again (with odataQueryContext and serialization in place) - Example:
[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()
}