【问题标题】:Returning a 404 from an explicitly typed ASP.NET Core API controller (not IActionResult)从显式类型的 ASP.NET Core API 控制器(不是 IActionResult)返回 404
【发布时间】:2017-05-18 18:52:42
【问题描述】:

ASP.NET Core API 控制器通常返回显式类型(如果您创建新项目,默认情况下会返回),类似于:

[Route("api/[controller]")]
public class ThingsController : Controller
{
    // GET api/things
    [HttpGet]
    public async Task<IEnumerable<Thing>> GetAsync()
    {
        //...
    }

    // GET api/things/5
    [HttpGet("{id}")]
    public async Task<Thing> GetAsync(int id)
    {
        Thing thingFromDB = await GetThingFromDBAsync();
        if(thingFromDB == null)
            return null; // This returns HTTP 204

        // Process thingFromDB, blah blah blah
        return thing;
    }

    // POST api/things
    [HttpPost]
    public void Post([FromBody]Thing thing)
    {
        //..
    }

    //... and so on...
}

问题在于 return null; - 它返回一个 HTTP 204:成功,没有内容。

这被很多客户端Javascript组件认为是成功的,所以有这样的代码:

const response = await fetch('.../api/things/5', {method: 'GET' ...});
if(response.ok)
    return await response.json(); // Error, no content!

在线搜索(例如this questionthis answer)指向有用的控制器扩展方法return NotFound();,但所有这些都返回IActionResult,这与我的Task&lt;Thing&gt; 返回类型不兼容。该设计模式如下所示:

// GET api/things/5
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
    var thingFromDB = await GetThingFromDBAsync();
    if (thingFromDB == null)
        return NotFound();

    // Process thingFromDB, blah blah blah
    return Ok(thing);
}

这行得通,但要使用它,必须将 GetAsync 的返回类型更改为 Task&lt;IActionResult&gt; - 显式类型丢失,并且控制器上的所有返回类型都必须更改(即在all) 或者会有一些操作处理显式类型而其他操作的混合。此外,单元测试现在需要对序列化做出假设,并明确反序列化 IActionResult 的内容,在它们具有具体类型之前。

有很多方法可以解决这个问题,但它似乎是一个很容易设计出来的令人困惑的混搭,所以真正的问题是:ASP.NET Core 设计者想要的正确方法是什么?

似乎可能的选项是:

  1. 根据预期的类型,有一个奇怪的(测试混乱)显式类型和 IActionResult 的组合。
  2. 忘记显式类型吧,Core MVC 并不真正支持它们,总是使用IActionResult(在这种情况下,它们为什么会出现?)
  3. 编写HttpResponseException 的实现并像ArgumentOutOfRangeException 一样使用它(有关实现,请参见this answer)。但是,这确实需要对程序流使用异常,这通常是个坏主意,而且deprecated by the MVC Core team
  4. 编写一个 HttpNoContentOutputFormatter 的实现,为 GET 请求返回 404
  5. Core MVC 的工作方式还缺少什么?
  6. 或者对于失败的 GET 请求,204 正确而 404 错误是否有原因?

这些都涉及妥协和重构,会丢失一些东西或增加一些看起来不必要的复杂性,与 MVC Core 的设计不一致。哪种妥协是正确的,为什么?

【问题讨论】:

  • @Hackerman 嗨,你读过这个问题吗?我特别了解StatusCode(500),它仅适用于返回IActionResult 的操作,然后我将详细介绍。
  • @Hackerman 不,它确实不是。 only 适用于IActionResult。我问的是显式类型 的操作。我继续在第一个要点中询问IActionResult 的使用,但我不是在问如何调用StatusCode(404) - 我已经知道并在问题中引用它。
  • 对于您的方案,解决方案可能类似于return new HttpResponseMessage(HttpStatusCode.NotFound); ...也根据此:docs.microsoft.com/en-us/aspnet/core/mvc/models/formatting For non-trivial actions with multiple return types or options (for example, different HTTP status codes based on the result of operations performed), prefer IActionResult as the return type.
  • @Hackerman 你投票结束了我的问题,因为这是我发现、阅读并经历过的一个问题,在我问这个问题之前和我在问题不是我正在寻找的答案。显然,我采取了防御措施——我想回答 我的 问题,而不是被指向一个圆圈。您的最终评论实际上很有用,并开始解决我实际要问的问题 - 您应该将其充实为完整的答案。
  • 好的,我得到了更多关于这个主题的信息......为了完成类似的事情(我仍然认为最好的方法应该是使用IActionResult),你可以按照这个例子@987654353 @ 如果thingnull,您可以返回HttpResponseException...

标签: c# asp.net-core asp.net-core-mvc http-status-code-404 asp.net-core-webapi


【解决方案1】:

这是addressed in ASP.NET Core 2.1ActionResult&lt;T&gt;

public ActionResult<Thing> Get(int id) {
    Thing thing = GetThingFromDB();

    if (thing == null)
        return NotFound();

    return thing;
}

甚至:

public ActionResult<Thing> Get(int id) =>
    GetThingFromDB() ?? NotFound();

我将在实施后更详细地更新此答案。

原答案

在 ASP.NET Web API 5 中有一个 HttpResponseException(正如 Hackerman 指出的那样),但它已从 Core 中删除,并且没有中间件来处理它。

我认为这种变化是由于 .NET Core - ASP.NET 尝试开箱即用地做所有事情,而 ASP.NET Core 只做你明确告诉它的事情(这是它如此重要的一个重要部分更快、更便携)。

我找不到执行此操作的现有库,因此我自己编写了它。首先我们需要一个自定义异常来检查:

public class StatusCodeException : Exception
{
    public StatusCodeException(HttpStatusCode statusCode)
    {
        StatusCode = statusCode;
    }

    public HttpStatusCode StatusCode { get; set; }
}

然后我们需要一个 RequestDelegate 处理程序来检查新异常并将其转换为 HTTP 响应状态代码:

public class StatusCodeExceptionHandler
{
    private readonly RequestDelegate request;

    public StatusCodeExceptionHandler(RequestDelegate pipeline)
    {
        this.request = pipeline;
    }

    public Task Invoke(HttpContext context) => this.InvokeAsync(context); // Stops VS from nagging about async method without ...Async suffix.

    async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await this.request(context);
        }
        catch (StatusCodeException exception)
        {
            context.Response.StatusCode = (int)exception.StatusCode;
            context.Response.Headers.Clear();
        }
    }
}

然后我们在Startup.Configure注册这个中间件:

public class Startup
{
    ...

    public void Configure(IApplicationBuilder app)
    {
        ...
        app.UseMiddleware<StatusCodeExceptionHandler>();

最后,动作可以抛出 HTTP 状态码异常,同时仍然返回一个显式类型,无需从 IActionResult 转换即可轻松进行单元测试:

public Thing Get(int id) {
    Thing thing = GetThingFromDB();

    if (thing == null)
        throw new StatusCodeException(HttpStatusCode.NotFound);

    return thing;
}

这会保留返回值的显式类型,并允许轻松区分成功的空结果 (return null;) 和因为找不到某些内容而导致的错误(我认为它就像抛出 ArgumentOutOfRangeException)。

虽然这是一个解决问题的方法,但它仍然没有真正回答我的问题 - Web API 的设计者构建了对显式类型的支持,并期望它们会被使用,添加了对 return null; 的特定处理,以便它会产生204而不是200,然后没有添加任何方法来处理404?添加这么基本的东西似乎需要做很多工作。

【讨论】:

  • 我认为如果路由资源有效,但没有返回任何东西,正确的响应应该是 204(No Content)....如果路由不存在应该返回一个404 响应(未找到)...有道理,对吧?
  • @Hackerman 我怀疑这很可能是一个选择,但是很多客户端 API 都泛化了(1xx 就可以了... 2xx 好的,3xx 在这里,4xx 你错了,5xx 我们明白了错误)- 2xx 表示一切正常,但实际上用户请求了一个不存在的资源(如我的问题中的示例,Fetch API 认为 204 可以继续)。我想 204 可能意味着正确的资源路径,错误的资源 ID,但这不是我的理解。任何引用作为所需的模式?
  • 看看这篇文章racksburg.com/choosing-an-http-status-code ...我认为状态码2xx/3xx的流程图解释得很好...你也可以看看其他的:)\
  • @Hackerman 这仍然表明 404 在这种情况下是正确的。
  • @Hackerman 我真的不确定——这似乎是试图在 API 中传达有关 API 的信息。如果我们这样做,我们不应该为没有请求操作的有效控制器实现 405(而不是 404)等等吗?在 REST 中,返回的是资源,而不是 API 本身。我认为这就是为什么约定名称是复数的原因(而不是像它们在数据库中那样的单数)-api/things/5 是期望成为单个事物的资源。
【解决方案2】:

您实际上可以使用IActionResultTask&lt;IActionResult&gt; 代替ThingTask&lt;Thing&gt; 甚至Task&lt;IEnumerable&lt;Thing&gt;&gt;。如果您有一个返回 JSON 的 API,那么您只需执行以下操作:

[Route("api/[controller]")]
public class ThingsController : Controller
{
    // GET api/things
    [HttpGet]
    public async Task<IActionResult> GetAsync()
    {
    }

    // GET api/things/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetAsync(int id)
    {
        var thingFromDB = await GetThingFromDBAsync();
        if (thingFromDB == null)
            return NotFound();

        // Process thingFromDB, blah blah blah
        return Ok(thing); // This will be JSON by default
    }

    // POST api/things
    [HttpPost]
    public void Post([FromBody] Thing thing)
    {
    }
}

更新

似乎担心的是,在返回 API 时显式会有所帮助,虽然可能是 显式,但实际上并不是很有用。如果您正在编写执行请求/响应管道的单元测试,您通常会验证原始返回(很可能是 JSON,即;C#中的字符串>)。您可以简单地将返回的字符串转换回强类型等效项,以便使用Assert 进行比较。

这似乎是使用IActionResultTask&lt;IActionResult&gt; 的唯一缺点。如果你真的,真的想要明确并且仍然想设置状态码,有几种方法可以做到这一点 - 但它不被认可,因为框架已经有一个内置的机制,即;在Controller 类中使用IActionResult 返回方法包装器。不过,你可以写一些 custom middleware 来处理这个问题。

最后,我想指出,如果根据W3 API 调用返回null,则204 的状态码实际上是准确的。为什么你会想要一个 404

204

服务器已完成请求,但不需要返回 实体主体,并且可能想要返回更新的元信息。这 响应可能包括新的或更新的元信息,形式为 entity-headers,如果存在应该与 请求的变体。

如果客户端是一个用户代理,它不应该改变它的文档视图 从导致发送请求的原因。这个回应是 主要目的是允许输入进行操作,而无需 导致用户代理的活动文档视图发生变化,尽管 任何新的或更新的元信息都应该应用于文档 当前在用户代理的活动视图中。

204 响应不能包含消息体,因此总是 由标题字段后的第一个空行终止。

我认为第二段的第一句话说得最好,“如果客户端是用户代理,它不应该改变导致发送请求的文档视图”。 API 就是这种情况。与 404 相比:

服务器没有找到任何匹配 Request-URI 的东西。不 指示该状况是暂时的还是 永恒的。如果服务器应该使用 410 (Gone) 状态码 通过一些内部可配置的机制知道,旧的 资源永久不可用且没有转发地址。 当服务器不希望这样做时,通常使用此状态码 准确说明请求被拒绝的原因,或者当没有其他请求时 响应是适用的。

主要区别是一个更适用于 API,另一个更适用于文档视图,即;显示的页面。

【讨论】:

  • 嗨,大卫,干杯。我意识到我可以切换到返回 IActionResult - 但如果这是答案,为什么 ASP.NET Core API 控制器在 IActionResult 确实需要时完全支持隐式类型转换?
  • 干杯大卫,但事实并非如此 - 该评论是直接引用我的问题。同样来自我的问题我不是在寻找 return NotFound(); 作为答案 - 这些仅适用于 IActionResult ...这似乎使显式返回类型毫无意义这就是我真正要问的,而不是“我如何使用NotFound()”而是“我应该如何返回404 显式输入” - 你的答案似乎是“不要使用显式输入”,但如果是的话缺少关于为什么显式类型是默认(甚至完全支持)的关键细节
  • 这应该是正确的答案...关于状态代码,有不同的实现(有些使用一种代码,有些使用另一种)根据您想要完成的内容以及如何...如果你想在某些情况下使用404,只要你的 api 有据可查,这应该不是问题。
  • 这不应该是正确的答案。如果您想 /get/{id} 并且服务器上没有该 id 的元素,则 404 - not found 是正确的答案。关于显式类型 - 该方法的类型信息实际上用于像 swagger 这样的工具,它们依赖于控制器声明来生成正确的 swagger 文件。所以 IActionResult 在这种情况下会丢失很多信息。
  • 幸好这没有被接受为正确答案。感谢您的评论。 :)
【解决方案3】:

为了完成类似的事情(不过,我认为最好的方法应该是使用IActionResult),您可以按照throwHttpResponseException 进行操作,如果您的Thingnull

// GET api/things/5
[HttpGet("{id}")]
public async Task<Thing> GetAsync(int id)
{
    Thing thingFromDB = await GetThingFromDBAsync();
    if(thingFromDB == null){
        throw new HttpResponseException(HttpStatusCode.NotFound); // This returns HTTP 404
    }
    // Process thingFromDB, blah blah blah
    return thing;
}

【讨论】:

  • 干杯 (+1) - 我认为这是朝着正确方向迈出的一步,但(在测试中)HttpResponseException 和处理它的中间件似乎不在 .NET Core 1.1 中.下一个问题是:我应该推出自己的中间件还是现有的(理想情况下是 MS)包已经这样做了?
  • 看起来这是在 ASP.NET Web API 5 中执行此操作的方法,但它已在 Core 中删除,考虑到 Core 的手动方法,这很有意义。在大多数情况下,Core 已经放弃了默认的 ASP 行为,您可以在 Startup.Configure 中添加一个新的可选中间件,但这里似乎没有。取而代之的是,从头开始编写一个似乎并不难。
  • 好的,我已经在此基础上充实了一个可行的答案,但仍然没有回答原始问题:stackoverflow.com/a/41484262/905
【解决方案4】:

当我想针对发送到请求中的错误数据返回 400 响应时,对于如何处理强类型响应的问题,我也一直在寻找答案。我的项目在 ASP.NET Core Web API (.NET5.0) 中。我找到的解决方案基本上是设置状态码并返回对象的default版本。这是您的示例,其中将状态代码设置为 404 并在 db 对象为空时返回默认对象。

[Route("api/[controller]")]
public class ThingsController : Controller
{
    // GET api/things
    [HttpGet]
    public async Task<IEnumerable<Thing>> GetAsync()
    {
        //...
    }

    // GET api/things/5
    [HttpGet("{id}")]
    public async Task<Thing> GetAsync(int id)
    {
        Thing thingFromDB = await GetThingFromDBAsync();
        if(thingFromDB == null)
        {
            this.Response.StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound;
            return default(Thing);
        }
        
        // Process thingFromDB, blah blah blah
        return thing;
    }

    // POST api/things
    [HttpPost]
    public void Post([FromBody]Thing thing)
    {
        //..
    }

    //... and so on...
}

【讨论】:

    【解决方案5】:

    有另一个相同行为的问题 - 所有方法都返回 404。问题在于缺少代码块

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-11
      • 2023-03-25
      • 1970-01-01
      • 2018-11-29
      • 1970-01-01
      相关资源
      最近更新 更多