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 (eg 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.
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.