Resolve ambiguous route issue - c#

I wish to implement 2 get routes with definition as:
GET /products - gets all products.
GET /products?name={name} - finds all products matching the specified name.
The code that I am using is:
[HttpGet] public IActionResult GetProducts()
and
[HttpGet] public IActionResult GetProductByName([FromQuery(Name = "name")] string name)
Can I achieve it?

Why not turn these into two separate endpoints one that is something like [HttpGet("Products")] and another endpoint that is [HttpGet("Products/{name}]"?
That way you can call your API like
.../Products and get all your Products back or .../Products/Name and get specific product information. You could also combine these into one endpoint [HttpGet("Products/{name}] and then return all your Products if name is null or empty.

Related

How to create multiple WebApi enpoints in ASP.NET Core

So i need to create three Endpoints that are -
/api/cities
/api/cities/:id
/api/cities?country=
The first endpoint should be when I type in -
www.mywebsite/api/cities
The second endpoint should be when I type in -
www.mywebsite/api/cities/1
The third endpoint should be when I type in -
www.mywebsite/api/cities?country=canada
I tried the following:
[HttpGet("")]
public async Task<IActionResult> GetCities() {/*...*/} // Should Get All Cities from DB
[HttpGet("{id:int}", Name="GetCityById")]
public async Task<IActionResult> GetCityById([FromRoute]int id) {/*...*/} // Should Get just one city from DB
[HttpGet("{country:alpha}")]
public async Task<IActionResult> GetCitiesByCountry([FromQuery]string country) {/*...*/} // Should get all cities from country
First two endpoints work well, but when I try to get all the cities from a country that I pass with query parameter, I seem to trigger the first endpoint that is /api/cities instead of /api/cities?country=
Before I get to the solution, you have one problem in your code. Defining [HttpGet("{country:alpha}")] on your last action is specifying a route template in which you expect a country route parameter to exist in the path portion of the URL (e.g. https://www.example.org/i/am/a/path). However, as you're marking string country with the FromQuery attribute, it will only bind country from the query string, and not the path.
Specifying the route template also means that when you send a request to /api/cities?country=blah it will never match your GetCitiesByCountry action, because it's expecting it to be in the form of /api/cities/country, as that's what you specified by using the route template. Therefore, the first thing you need to do is to change your last action to:
[HttpGet]
public async Task<IActionResult> GetCitiesByCountry([FromQuery] string country)
Now that's out of the way, it still won't work. The problem comes down to this: only the path portion of a request's URL is considered when selecting an action. That means, now that we've removed the route template for GetCitiesByCountry, a request of /api/cities, or /api/cities?country=blah, will return a server 500 error, because both GetCities and GetCitiesByCountry are a match for those requests, and the framework doesn't know which one to execute.
To allow this to work, we need to use what's called an action constraint. In this case, we want to specify that GetCities should only be considered a match if there is no query string present in the request. To do that, we can inherit from ActionMethodSelectorAttribute:
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Routing;
public class IgnoreIfRequestHasQueryStringAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
return !routeContext.HttpContext.Request.QueryString.HasValue;
}
}
And then decorate GetCities with it:
[HttpGet]
[IgnoreIfRequestHasQueryString]
public async Task<IActionResult> GetCities()
If we now send a request to /api/cities?country=blah, GetCities will be excluded from selection because the request has a query string, and so GetCitiesByCountry will execute just as we want. If we send a request to /api/cities, however, we might expect that we still get the ambiguous action selection error, because the request does not contain a query string and so it would seem both GetCities and GetCitiesByCountry are still a match for the request. But the request does actually succeed, and runs GetCities as we want.
The reason for this can be summed up by the following remark on the IActionConstraint type, which ActionMethodSelectorAttribute itself implements:
Action constraints have the secondary effect of making an action with a constraint applied a better match than one without. Consider two actions, 'A' and 'B' with the same action and controller name. Action 'A' only allows the HTTP POST method (via a constraint) and action 'B' has no constraints. If an incoming request is a POST, then 'A' is considered the best match because it both matches and has a constraint. If an incoming request uses any other verb, 'A' will not be valid for selection due to it's constraint, so 'B' is the best match.
That means because we defined a custom constraint of IgnoreIfRequestHasQueryString, which succeeds in matching /api/cities, the GetCities method is considered to be the preferred choice, and so there is no ambiguity.

c# Rest GET Endpoint with GetOne and GetAll

I have a very simple endpoint as an example
/users/
to obtain all users, wich returns an array of N elements (maybe 0 elements if no users)
this is implemented on one specific method
[HttpGet]
[Route("api/users")]
public void getUsers() { ... }
But then, i need to get a user by id, so i would need another api enpoint. The question is, should i implement this on the same method, or a separate one ?
[HttpGet]
[Route("api/users")]
[Route("api/users/{id:int}")]
public void getUsers(int? id) { ... }
In this case, my return is different, a single user or a NotFound instead of an array.
So i was wondering if this should be implemented as a single method (and reuse code) or 2 different methods.
2 different methods in case if response is going to be different. It looks like one method is to get all users details and another is to get specific user details based on user id.

Conflicting routes in ASP.NET Core when using Attribute based routing

Context
I am trying to build an ASP.NET Core web API controller that will expose the following methods with specific semantics:
/api/experimental/cars — get entire collection
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> AllCars()
/api/experimental/cars/123 — get car by id "123"
[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)
/api/experimental/cars?nameFilter=Maz — get cars that match nameFilter = "Maz"
[HttpGet("/api/experimental/cars/{nameFilter?}")]
public Task<List<Car>> CarsByNameFilter([FromQuery] string nameFilter = "")
/api/experimental/cars?nameFilter=Maz&rating=2 — get cars that match nameFilter = "Maz" and with rating greater or equal to 2
[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<List<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
Note: I really want to keep the controller class clean and have a single method per Web API route — is it possible?
Problem
As you could guess, there's an issue with these API definitions. Basically, AllCars is intercepting pretty much all the requests. (When I was able to get at least the /api/experimental/cars/{carId} working, the query-string based APIs were still not working and intercepted by another method...
I tried many possible route syntaxes to express what I want with no luck. Is it even possible to use the default routing mechanism or I need to implement my own Router class or Middleware or something else?
Update 1: Problem definition
I know I can join at least three methods and their routes into a single WebAPI method that is being smart about the received parameters. Notice that this is exactly what I am trying to avoid.
Why?
Reason 1: I saw that in non-.NET routers, it worked well and there's no technical impossibility to implement semantic based route resolution.
Reason 2: I perceive all four URL patterns mentioned above as four different routes. One may not agree with me and it's okay, but for my purposes the methods and the routes are different and have to stay different.
Reason 3.1: This keeps controller code clean. Every method only handles one specific case. Parameter names are sufficient to properly resolve the routes (at least in humans head, therefore machine can do it too -- it's easy to formalize the algorithm). If client make a request with an unsupported query parameter, it should result in HTTP 404 Not Found or HTTP 400 Bad Request -- totally fine (client rather construct correct URLs).
Reason 3.2: On contrary, if I join the methods and use a more generic route, my implementation needs to be 'smart' about the combination of parameters. This is effectively, a leak of routing abstractions into a layer where it does not belong in my architecture. Complex validation is another thing I don't want to see in the Controller -- less code is better.
Update 2: Nancy — Another .NET example (other that .NET Core WebApi)
There is Nancy (a .NET framework) which perfectly deals with this aspect of routing: https://github.com/NancyFx/Nancy/wiki/Defining-routes#pattern The issue is that in my project we're not using it... Nancy works as a perfect example of a tool that leaves exact definition of routing semantics to the client, instead of enforcing too tight rules on what is the route vs what is not.
You could Achieve this with just two routes:
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
and
[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)
I.e one route which brings the entire set back but can be filtered accordingly and one the brings back a single Car object by Id.
You will notice that the SearchCars method doesn't include the parameters in the route, FromQuery will catch these anyway.
EDIT:
if your request becomes complex it can be nice to define a custom request object type to wrap all your filters together:
public class MyRequestObject
{
public string NameFilter {get;set;}
public int Rating {get;set;}
}
then:
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] MyRequestObject requestParams)
Take a look at the following suggested routes that when tested do not conflict with each other and still allow for all the actions to be segregated.
[Route("api/experimental/cars")]
public class CarsController : Controller {
//GET api/experimental/cars
[HttpGet("")]
public IActionResult AllCars() { ... }
//GET api/experimental/cars/123
[HttpGet("{carId}")]
public IActionResult CarById(string carId) { ... }
//GET api/experimental/cars/named/Maz
//GET api/experimental/cars/named?filter=Maz
[HttpGet("named/{filter?}")]
public IActionResult CarsByNameFilter(string filter = "") { ... }
//GET api/experimental/cars/filtered?rating=2&name=Maz
//GET api/experimental/cars/filtered?rating=2
//GET api/experimental/cars/filtered?name=Maz
[HttpGet("filtered")]
public IActionResult CarsByNameAndRatingFilter(string name = "", int rating = 1) { ... }
}
My experience with this topic tells me that the best way to implement the APIs I wanted is to have two methods:
class CarsController {
// [HttpGet("/api/experimental/cars/")]
[HttpGet("/api/experimental/cars/{carId}")]
public Task<IEnumerable<Car>> CarById([FromRoute] string carId)
{
if (carId == null)
return GetAllCars();
else
return GetCarWithId(carId);
}
// [HttpGet("/api/experimental/cars/{nameFilter?}")]
[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<IEnumerable<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
{
// TODO Validate the combination of query string parameters for your specific API/business rules.
var filter = new Filter {
NameFilter = nameFilter,
Rating = rating
};
return GetCarsMatchingFilter(filter);
}
}
The first API is almost trivial. Even though returning a single item within a wrapping collection object may not look nice, it minimizes the number of API methods (which I personally am fine with).
The second API is trickier: in a way, it works as the façade pattern. I.e. that API will respond to pretty much all the possible /api/experimental/cars? based routes. Therefore, we need to very carefully validate the combination of received arguments before doing the actual work.

RoutePrefix Order alternative for WebAPI 2

In WebAPI you can specify an Order in RouteAttribute to determine which order the routes are matched in. For example the below will match /other to GetOther before matching /blah to GetByName
[HttpGet, Route("{name}", Order = 1)]
public string GetByName(string name) { ... }
[HttpGet, Route("other")]
public string GetOther() { ... }
How would I do the same but with RoutePrefix (which doesn't have an Order property)? If it did it would looks something like this:
[RoutePrefix("foo", Order = 1)]
public class FooController : ApiController { ... }
[RoutePrefix("foo/bar")]
public class FooBarController : ApiController { ... }
Doing the above (without the imaginary Order property) throws the following message when calling /foo/bar:
Multiple controller types were found that match the URL
Is there existing functionality for getting around this (preferably with attributes)?
I don't believe Microsoft's attribute routing has support for ordering routes by controller.
When you specify an Order property on an action's RouteAttribute, you are specifying the order within the controller only.
AFAIK, the attribute routing algorithm will scan all of the controllers alphabetically. Then within each controller, is will use the Order property of any RouteAttributes to decide the order of action routes within that controller.
This means if you have route collisions spread across different controllers, you should either rethink the design or make sure the controllers with the more specific route patterns are named alphabetically before the controllers with the more general route patterns. Otherwise, you may run into that "ambiguous route / multiple actions with matching routes found" exception.
Update: The answer above is for Microsoft's AttributeRouting implementation, which was based on another very popular open source project that came before MVC5. In that library, you could order attribute routes by controller, though I think the property was SiteOrder or something like that.
You can add a orderby to the loop in index.cshtml:
#foreach (var group in apiGroups.OrderBy(g => g.Key.ControllerName))

WebAPI controller same action different parameters

I have a base controller as with 2 actions:
[ActionName("Find")]
[HttpGet]
public virtual IHttpActionResult Find(string name)
{
return null;
}
[ActionName("Find")]
[HttpGet]
public virtual IHttpActionResult Find(int number)
{
return null;
}
Some of my controllers use the different Find method, for example:
public override IHttpActionResult Find(string number)
{
return OK;
}
However, I get an error when calling this action from the client:
Multiple actions were found that match the request: \r\nFind on type API.Controllers.CustomerController
How can I solve this problem?
The only way to solve this is by changing the ActionName attribute for one of the actions.
ASP.NET MVC /Web API doesn't support two actions with the same name and same HTTP verb in the same controller.
Also take a look at this question (ASP.NET MVC ambiguous action methods) if you want to go for the 'hack solution' (my opinion).
Why don't you just pass both parameters to the same action method? And inside your method simply check if they are null and do something about it.
Use string and int? (nullable int) to allow both parameters to contain nulls.
This way you get to use one view without any attribute jiggery pockery.
I think, you should reconsider your endpoint structure:
An action that selects one element from a resource collection should do that along the key of the resource (i.e. the unique database key). This key can be either of type int or alphanum, but not both.
What you probably want to realize with one or both of your finds, is to establish a filter function. Filter parameters should be passed to REST endpoints as query string parameters.
Examples:
/api/employees → returns resource set with all employees
/api/employees/5 → returns single resource (one employee)
/api/employees?name=john → returns resource set with all employees named "john"
Example 3 is a filter, and I guess at least one of your finds is just that.

Categories

Resources