简体   繁体   中英

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

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM