如何在 ASP.NET Core 中创建多个 Web Api 端点

How to create multiple WebApi enpoints in ASP.NET Core

所以我需要创建三个端点 -
/api/cities
/api/cities/:id
/api/cities?国家=

第一个端点应该是当我输入 -
www.mywebsite/api/cities

第二个端点应该是当我输入 -
www.mywebsite/api/cities/1

第三个端点应该是当我输入 -
www.mywebsite/api/cities?国家=加拿大

我尝试了以下方法:

[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

前两个端点运行良好,但是当我尝试从我通过查询参数传递的国家/地区获取所有城市时,我似乎触发了第一个端点 /api/cities 而不是 /api/cities?country=

在我找到解决方案之前,您的代码中有一个问题。在您的最后一个操作中定义 [HttpGet("{country:alpha}")] 是指定一个路由模板,您希望 country 路由参数存在于 URL 的路径部分(例如 https://www.example.org/i/am/a/path)。但是,当您使用 FromQuery 属性标记 string country 时,它只会绑定查询字符串中的 country,而不是路径。

指定路由模板还意味着当您向 /api/cities?country=blah 发送请求时,它永远不会匹配您的 GetCitiesByCountry 操作,因为它期望它采用 /api/cities/country 的形式,因为这是您使用路由模板指定的内容。因此,您需要做的第一件事就是将您的最后一个操作更改为:

[HttpGet]
public async Task<IActionResult> GetCitiesByCountry([FromQuery] string country)

既然这样了,还是不行。问题归结为:选择操作时仅考虑请求 URL 的 path 部分。这意味着,既然我们已经删除了 GetCitiesByCountry 的路由模板,/api/cities/api/cities?country=blah 的请求,将 return 出现服务器 500 错误,因为两者 GetCitiesGetCitiesByCountry 匹配那些请求,框架不知道要执行哪一个。

为了让它起作用,我们需要使用所谓的动作约束。在这种情况下,我们要指定 GetCities 只有在请求中不存在查询字符串时才应被视为匹配项。为此,我们可以继承 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;
    }
}

然后用它装饰GetCities:

[HttpGet]
[IgnoreIfRequestHasQueryString]
public async Task<IActionResult> GetCities()

如果我们现在向 /api/cities?country=blah 发送请求,GetCities 将被排除在选择之外,因为该请求具有查询字符串,因此 GetCitiesByCountry 将按照我们的意愿执行。但是,如果我们向 /api/cities 发送请求,我们可能会期望我们仍然会收到不明确的操作选择错误,因为该请求不包含查询字符串,因此看起来 GetCitiesGetCitiesByCountry 仍然匹配请求。但是请求确实成功了,并且按照我们的意愿运行 GetCities

其原因可以总结为以下 remark on the IActionConstraint typeActionMethodSelectorAttribute 本身实现:

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.

也就是说因为我们定义了自定义约束IgnoreIfRequestHasQueryString,成功匹配了/api/cities,所以认为GetCities方法是首选,所以没有歧义。