CreatedAtAction会抛出“没有与提供的值匹配的路由”,尽管路由和参数似乎是正确的
我有一个使用API版本控制的简单ASP.NET Core Web API控制器。我的POST操作尝试返回一个 CreatedAtAction,但总是抛出:
[HttpPost]
public async Task<IActionResult> AddCarTypeAsync(
[FromRoute] ApiVersion apiVersion,
[FromBody] CarTypeCreateRequest request,
CancellationToken ct)
{
await _validateCreate.ValidateAndThrowAsync(request, ct);
var carType = await _carTypeService.AddAsync(request.ToDto(), ct);
return CreatedAtAction(
nameof(GetCarTypeByIdAsync),
new { version = apiVersion, carTypeId = carType.Id },
carType .ToResponse());
}
以及它应该链接到的 GET 操作:
[HttpGet("{carTypeId}", Name = nameof(GetCarTypeByIdAsync))]
public async Task<IActionResult> GetCarTypeByIdAsync(
[FromRoute] ApiVersion apiVersion,
[FromRoute] Guid carTypeId,
CancellationToken ct)
{
if (carTypeId == Guid.Empty)
return BadRequest("carTypeId can't be Guid Empty");
var carType = await _carTypeService.GetByIdAsync(carTypeId, ct);
return carType != null ? Ok(carType .ToResponse()) : NotFound();
}
控制器具有以下路由:
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/car-types")]
但 CreatedAtAction 仍然抛出标题中所示的异常。
我无法弄清楚为什么路由没有被匹配到。这个设置里我还缺少什么?
解决方案
正如 @ZdeněkJelínek在注释中提到的,你的问题很可能是由 SuppressAsyncSuffixInActionNames 引起的,该选项自ASP.NET Core 3.0以来默认被设置为 true(查看文档)。
实际上,这意味着在路由层面,框架会在内部从你的操作方法名中移除 Async 后缀。因此,在路由表中,GetCarTypeByIdAsync 变成了 GetCarTypeById。然而,在编译期间对 nameof(GetCarTypeByIdAsync) 进行求值时,它始终返回完整文本 “GetCarTypeByIdAsync”,如果仍然没有匹配到任何内容。这就是出现 “No route matches” 错误的地方。
为了解决 aspnetcore#4849,这是在3.0版本中的一次 破坏性变更。在你的情况下,最简单的解决方案是 切换到 CreatedAtRoute。你的 GET 操作已经定义了一个路由名称(Name = nameof(GetCarTypeByIdAsync)),且后缀剥离对路由名称没有影响。
return CreatedAtRoute(
nameof(GetCarTypeByIdAsync),
new { version = apiVersion.ToString(), carTypeId = carType.Id },
carType.ToResponse());
因为 CreatedAtRoute 直接按路由名称进行匹配,避免了整个动作名解析过程带来的麻烦,这就起作用了。此外,由于 CreatedAtAction 必须在所有内部框架修改之后,才匹配动作名+ 控制器+ 路由值,而 CreatedAtRoute 只需要查找一个唯一名称,因此对于版本化API来说通常更合适。
另外,请注意,在 apiVersion 上的 .ToString() 将其作为字符串传递,以确保在序列化为路由段时能够正确处理。
如果你想继续使用 CreatedAtAction,你有两种选择:
自行剥离后缀:
return CreatedAtAction(
"GetCarTypeById", // without "Async"
new { version = apiVersion.ToString(), carTypeId = carType.Id },
carType.ToResponse());
或者全局禁用剥离(仅为修复一次调用而进行此操作我不推荐这样做):
builder.Services.AddControllers(options =>
{
options.SuppressAsyncSuffixInActionNames = false;
});
本地重现了该问题并验证了 CreatedAtRoute 生成了正确的Location头。