Multiple get methods in a .Net WebApi OData Controller

Viewed 1143

I would like to know if there is the possibility to implement two different get methods with parameter in the same ODataController. This is what I am trying to do, but it is not working. What could I do?

In a ODataController:

public class ProductsController : ODataController
{
    private readonly ProductsService _service;
    
    public ProductsController()
    {
        _service = new ProductsService();
    }

[EnableQuery]
public IQueryable<Product> Get()
{
    return _service.GetAll();
}

[EnableQuery]
public Product Get(int key)
{
    return _service.GetById(key);
}

[EnableQuery]
public IQueryable<Product> Get(string key)
{
    return _service.GetByFilter(key);
}

In the WebApiConfig.cs file I have the next configuration:

ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
config.Routes.MapODataServiceRoute("odata", "api/odata", builder.GetEdmModel());

I can use both the 'get' method without parameter with one of the 'get' methods with parameter, but I cannot use together the both methods with parameter. I have seen the OdataRoute but I did not get to function. I know that I could use the OData filter functionality with the generic method, but I would like to try without using it. Is there another solution?

Thank you!

1 Answers

OData Routes do not support overloads OOTB which is effectively what you are describing here, two GET Collection methods, but you want one of them to have the same route as the GET Resource method.

The first question is why do you want to have the same route pointing to two different methods, especially when one of the routes returns a single resource, and the other will return a collection.

Built-In OData Routing Conventions
The built in conventions will route known URLs to methods that match the expected signatures. All other routes need to be registered with the Edm Model either as discrete Functions or Actions. You could also implement your own routes

The arguments are more or less the same if you had intended the Get(string key) to return a single item or a collection. There is still a clash between a default route and 2 different methods to handle the request for that route. There is a direct duplicate isssue on SO for this type of issue: OData route exception

In OData we make a distinction between the key of a resource, that fits into the default ~/Controller(key) route and effectively all other routes. For all other non-default routes, we simply need to declare them in the OData EdmModel to make them valid

There are other possible solutions like Implementing your own version of the DefaultODataPathHandler as explained here or a custom IODataRoutingConvention as explained here.
However I find it best to try and stick to the OData standard conventions as much as possible, so when faced with a routing issue where there can be defined a simple mapping between the OData Conventional way and our business need, AND you want to support this syntax in a global sense, then you can use a simple url rewrite module.

  1. Declare your custom method endpoint as a separate Function in the Edm Model.
  2. Create a Url Rewrite to map between legacy routes and the OData routes.

Declare a Custom Function in the Edm Model:

To access your custom function via the Edm Model, there are two changes we need to make:

  1. change the method name to something other than Get. In this example we will use the term Search

     [HttpGet]
     [EnableQuery]
     public IQueryable<Product> Search(string key)
     {
         return _service.GetByFilter(key);
     }
    
  2. modify your builder fluent notation:

    ODataModelBuilder builder = new ODataConventionModelBuilder();
    var products = builder.EntitySet<Product>("Products");
    products.EntityType.Collection.Function(nameof(ProductsController.Search))
                                   .ReturnsCollectionFromEntitySet<Facility>("Products")
                                   .Parameter<string>("key");
    config.Routes.MapODataServiceRoute("odata", "api/odata", builder.GetEdmModel());
    

    NOTE: The name of the route must match the name of the method for this configuration to work, to enforce this nameof(ProductsController.Search) was used however, "Search" is all that is necessary.

The final URL that will match this template:

GET: ~/api/odata/Products/Search(key='Foo')

Url Rewrite Module

public class ODataConventionUrlRewriter : OwinMiddleware
{
    /// <summary>
    /// Create a Url Rewriter to manipulate incoming OData requests and rewrite or redirect to the correct request syntax
    /// </summary>
    /// <param name="next"></param>
    public ODataConventionUrlRewriter(OwinMiddleware next)
        : base(next)
    {
    }
    /// <summary>
    /// Process the incoming request, if it matches a known path, rewrite accordingly or redirect.
    /// </summary>
    /// <param name="context">OWin Request context to process</param>
    /// <returns>Chains the next processor if the url is OK or rewritten, otherwise the response is to redirect</returns>
    public override async Task Invoke(IOwinContext context)
    {
        // Match ANY /Controller(NonNumeric)
        // Rewrite to /Controller/Search(key='NonNumeric')
        var regex = new System.Text.RegularExpressions.Regex(@"\(([^\d=\'\(]+)\)$");
        match = regex.Match(context.Request.Path.Value);
        if (match != null && match.Success)
        {
            // We have to use redirect here, we can't affect the query inflight
            context.Response.Redirect($"{context.Request.Uri.GetLeftPart(UriPartial.Authority)}{regex.Replace(context.Request.Path.Value, $"/Search(key='{match.Groups[1].Value}')")}");
        }
        else
            await Next.Invoke(context);
    }
}

Finally, in the StartUp.cs or where your OWin context is configured add this module into the pipeline before the OData config:

public void Configuration(IAppBuilder app)
{
    ...
    // Rewrite URLs
    app.Use(typeof(ODataConventionUrlRewriter));
    ...
    // Register routes
    config.MapHttpAttributeRoutes();
    
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    var products = builder.EntitySet<Product>("Products");
    products.EntityType.Collection.Function(nameof(ProductsController.Search))
                                  .ReturnsCollectionFromEntitySet<Facility>("Products")
                                  .Parameter<string>("key");
    config.Routes.MapODataServiceRoute("odata", "api/odata", builder.GetEdmModel());

    ...
    // Start Web API
    app.UseWebApi(config);

}

Now the final Url that is supported for our custom Function is this:

GET: ~/api/odata/Products(Foo)

Try to use the conventions when you can, you have chosen OData for a reason, if you do use a rewrite module, try to be as specific in your conditional logic as you can be to avoid breaking other standard routes that might be in use.

I try to reserve rewrite solutions for common legacy query routes.

Related