CreatedAtAction会抛出“没有与提供的值匹配的路由”,尽管路由和参数似乎是正确的

后端开发 2026-07-08

我有一个使用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头。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章