如何在 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 错误,因为两者 GetCities
和 GetCitiesByCountry
匹配那些请求,框架不知道要执行哪一个。
为了让它起作用,我们需要使用所谓的动作约束。在这种情况下,我们要指定 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
发送请求,我们可能会期望我们仍然会收到不明确的操作选择错误,因为该请求不包含查询字符串,因此看起来 GetCities
和 GetCitiesByCountry
仍然匹配请求。但是请求确实成功了,并且按照我们的意愿运行 GetCities
。
其原因可以总结为以下 remark on the IActionConstraint
type,ActionMethodSelectorAttribute
本身实现:
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
方法是首选,所以没有歧义。
所以我需要创建三个端点 -
/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 错误,因为两者 GetCities
和 GetCitiesByCountry
匹配那些请求,框架不知道要执行哪一个。
为了让它起作用,我们需要使用所谓的动作约束。在这种情况下,我们要指定 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
发送请求,我们可能会期望我们仍然会收到不明确的操作选择错误,因为该请求不包含查询字符串,因此看起来 GetCities
和 GetCitiesByCountry
仍然匹配请求。但是请求确实成功了,并且按照我们的意愿运行 GetCities
。
其原因可以总结为以下 remark on the IActionConstraint
type,ActionMethodSelectorAttribute
本身实现:
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
方法是首选,所以没有歧义。