简体   繁体   中英

Mapping dynamic odata routes with ASP.NET Core OData 8.0

I've got an application where the EDM datatypes are generated during the runtime of the application (they can even change during runtime). Based loosely on OData DynamicEDMModelCreation Sample - refactored to use the new endpoint routing. There the EDM model is dynamically generated at runtime and all requests are forwarded to the same controller.

Now I wanted to update to the newest ASP.NET Core OData 8.0 and the whole routing changed so that the current workaround does not work anymore.

I've read the two blog posts of the update Blog1 Blog2 and it seems that I can't use the "old" workaround anymore as the function MapODataRoute() within the endpoints is now gone. It also seems that none of the built-in routing convention work for my use-case as all require the EDM model to be present at debug time.

Maybe I can use a custom IODataControllerActionConvention . I tried to active the convention by adding it to the Routing Convention but it seems I'm still missing a piece how to activate it.

services.TryAddEnumerable(
    ServiceDescriptor.Transient<IODataControllerActionConvention, MyEntitySetRoutingConvention>());

Does this approach even work? Is it even possible to activate a dynamic model in the new odata preview? or does anybody has a slight idea how to approach a dynamic routing for the new odata 8.0?

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 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) add a AddCustomODataService(services) and 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()
}

There is an example for dynamic routing and dynamic model here:

https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataDynamicModel/ See MyODataRoutingApplicationModelProvider and MyODataRoutingMatcherPolicy which will pass a custom IEdmModel to the controller.

The HandleAllController can handle different types and edm models in a dynamic way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM