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.
- Declare your custom method endpoint as a separate Function in the Edm Model.
- 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:
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);
}
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.