Search code examples
c#asp.netasp.net-web-apiodata

Pass Parameters in OData WebApi Url


Using Web Api I have an OData EndPoint which can return Products from a database.

I have multiple databases with similar schemas, and want to pass a parameter in the URL to identify which database the Api should use.

Current Odata Endpoint:
http://localhost:62999/Products

What I want:
http://localhost:62999/999/Products

In the new Url, I pass in 999 (the database ID).

The database ID is intended to specify which database to load the product from. For example localhost:62999/999/Products('ABC123') would load product 'ABC123' from database 999, but the next request, localhost:62999/111/Products('XYZ789') would load the product 'XYZ789' from database 111.

The Url below works, but I don't like it.
localhost:62999/Products('XYZ789')?database=111

Here is the code for the controller:

public class ProductsController : ErpApiController //extends ODataController, handles disposing of database resources
{
    public ProductsController(IErpService erpService) : base(erpService) { }

    [EnableQuery(PageSize = 50)]
    public IQueryable<ProductDto> Get(ODataQueryOptions<ProductDto> queryOptions)
    {
        return ErpService.Products(queryOptions);
    }

    [EnableQuery]
    public SingleResult<ProductDto> Get([FromODataUri] string key, ODataQueryOptions<ProductDto> queryOptions)
    {
        var result = ErpService.Products(queryOptions).Where(p => p.StockCode == key);
        return SingleResult.Create(result);
    }               
}

I use Ninject to resolve which implementation of IErpService to inject into the controller by binding to a service provider:

kernel.Bind<IErpService>().ToProvider(new ErpServiceProvider()); And the ErpServiceProvider inspects the url to identify the databaseId required by this request:

public class ErpServiceProvider : Provider<IErpService>
{
    protected override IErpService CreateInstance(IContext context)
    {
        var databaseId = HttpContext.Current.Request["database"];

        return new SageErpService(new SageContext(GetDbConnection(databaseId)));
    }
}

The bit I am stuck on is how to define the Url parameter in the OData route config.

Normal WebApi routes can have parameters defined as follows:

config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
      );

But how do I define the parameters in the OData route config?

ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<ProductDto>("Products");
        builder.EntitySet<WorkOrderDto>("WorkOrders");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());

Is this even where I should be defining the Url parameters? I have also thought about using a Message Handler but I am not certain how this can be implemented either.

UPDATE
This question is trying to do the same thing as me: How to declare a parameter as prefix on OData
But it is not clear how the parameter is to be read from the url.
var databaseId = HttpContext.Current.Request["database"]; returns null currently.
Even after updating the route config to the following:

public static void Register(HttpConfiguration config)
{
    // Web API routes
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "ErpApi",
        routeTemplate: "{database}/{controller}"                
    );

    // Web API configuration and services
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<ProductDto>("Products");
    builder.EntitySet<WorkOrderDto>("WorkOrders");
    config.MapODataServiceRoute(
        routeName: "ODataRoute",
        routePrefix: "{company}/",
        model: builder.GetEdmModel());

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );
}

Solution

  • I've encountered a solution to pass dynamic parameter on OData, not sure if is the right one.

    I've used this solution on a certain context, where the dynamic parameter was just to authenticate the client, but I think you can solve your problem in a similar way.

    Problem: You wan't to pass a dynamic value at the URL request example: http://localhost:62999/{dynamicValue}/Products('ABC123'), but the ODataRouting will never route correctly, because of that extra /{dynamicValue} and the ODataControler "will not hit". Using ApiController you could made a custom routing, but at OData you can't (at least I didn't found an easy way to do it, probably you had to made your own or extend the OData routing convention).

    So as alternative solution: If every request will have a dynamicValue for example: "http://localhost:62999/{dynamicValue}/Products" do the following steps:

    1. Before routing the request Extract the dynamicValue (In my case, I've used an IAuthenticationFilter to intercept the message before it was routed, since the parameter was related with authorization, but maybe for your case it makes more sense to use another thing)
    2. Store the dynamicValue (somewhere on the request context)
    3. Route the ODataController without the {dynamicValue}. /Products('ABC123') instead of /{dynamicValue}/Products('ABC123')

    Here is the code:

    // Register the ServiceRoute
    public static void Register(HttpConfiguration config)
    {
    
      // Register the filter that will intercept the request before it is rooted to OData
      config.Filters.Add(CustomAuthenticationFilter>()); // If your dynamic parameter is related with Authentication use an IAuthenticationFilter otherwise you can register a MessageHandler for example.
    
      // Create the default collection of built-in conventions.
      var conventions = ODataRoutingConventions.CreateDefault();
    
      config.MapODataServiceRoute(
              routeName: "NameOfYourRoute",
              routePrefix: null, // Here you can define a prefix if you want
              model: GetEdmModel(), //Get the model
              pathHandler: new CustomPathHandler(), //Using CustomPath to handle dynamic parameter
              routingConventions: conventions); //Use the default routing conventions
    }
    
    // Just a filter to intercept the message before it hits the controller and to extract & store the DynamicValue
    public class CustomAuthenticationFilter : IAuthenticationFilter, IFilter
    {
       // Extract the dynamic value
       var dynamicValueStr = ((string)context.ActionContext.RequestContext.RouteData.Values["odatapath"])
            .Substring(0, ((string)context.ActionContext.RequestContext.RouteData.Values["odatapath"])
            .IndexOf('/')); // You can use a more "safer" way to parse
    
       int dynamicValue;
       if (int.TryParse(dynamicValueStr, out dynamicValue))
       {
          // TODO (this I leave it to you :))
          // Store it somewhere, probably at the request "context"
          // For example as claim
       } 
    }
    
    // Define your custom path handler
    public class CustomPathHandler : DefaultODataPathHandler
    {
        public override ODataPath Parse(IEdmModel model, string serviceRoot, string odataPath)
        {
            // Code made to remove the "dynamicValue"
            // This is assuming the dynamicValue is on the first "/"
            int dynamicValueIndex= odataPath.IndexOf('/');
            odataPath = odataPath.Substring(dynamicValueIndex + 1);
    
            // Now OData will route the request normaly since the route will only have "/Products('ABC123')"
            return base.Parse(model, serviceRoot, odataPath);
        }
    }
    

    Now you should have the information of the dynamic value stored at the context of the request and OData should route correctly to the ODataController. Once your there at your method, you can access the request context to get information about the "dynamic value" and use it to choose the correct database