从显式类型的ASP.NET Core API控制器(不是IActionResult)返回404


74

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!

在线搜索(例如,这个问题这个答案)指向return NotFound();了控制器有用的扩展方法,但是所有这些returnIActionResult都与我的Task<Thing>return类型不兼容。该设计模式如下所示:

// 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<IActionResult>-显式类型丢失,并且控制器上的所有返回类型都必须更改(即完全不使用显式类型),否则将会混合使用一些动作处理显式类型,而另一些动作。另外,单元测试现在需要对序列化进行假设,并IActionResult在它们具有具体类型之前显式地反序列化where的内容。

有很多解决方法,但是似乎很容易混淆,很容易设计出来,所以真正的问题是:ASP.NET Core设计者打算使用的正确方法什么?

似乎可能的选择是:

  1. 混合使用显式类型和IActionResult预期类型,这很奇怪(要测试)。
  2. 忘记显式类型,Core MVC并不真正支持它们,而是始终使用IActionResult(在这种情况下,为什么它们全部存在?)
  3. 编写的实现,HttpResponseException然后像使用它ArgumentOutOfRangeException(有关实现,请参见此答案)。但是,这确实需要在程序流中使用异常,这通常是一个坏主意,并且MVC Core团队也不建议使用
  4. 为GET请求编写HttpNoContentOutputFormatter该返回的实现404
  5. 在Core MVC应该如何工作的过程中,我还缺少其他东西?
  6. 还是有一个原因导致失败的GET请求204正确与404错误?

所有这些都涉及到折衷和重构,这些折衷和重构会丢失某些东西或增加似乎不必要的复杂性,这与MVC Core的设计不一致。哪种妥协是正确的,为什么?


1
@Hackerman嗨,您读过问题吗?我特别清楚,StatusCode(500)它只能与return一起使用IActionResult,然后再详细介绍。
基思(Keith)

1
@Hackerman不,具体不是。这不仅可与IActionResult。我在问具有显式类型的动作。我继续询问IActionResult第一个要点的用法,但我没有问如何调用StatusCode(404)-我已经知道并在问题中引用了它。
基思

1
对于您的方案,解决方案可能类似于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

1
@Hackerman,您投票结束了我的问题,这是对我提出该问题以及我在该问题中回答的不是我正在寻找的答案之前发现,阅读并经历的一个问题的重复。显然,我一直处于防御状态-我想回答我的问题,而不是一再指出。您的最终评论实际上很有用,并且可以解决我真正要问的问题-您应该充实它,以得到完整的答案。
基思

1
好的,我有关IActionResult于此主题的更多信息...为了完成类似的工作(仍然我认为最好的方法应该使用),您可以按照以下示例操作public Item Get(int id) { var item = _repo.FindById(id); if (item == null) throw new HttpResponseException(HttpStatusCode.NotFound); return item; } ,在该示例中可以返回HttpResponseExceptionif thingis null...
Hackerman

Answers:


73

在ASP.NET Core 2.1中通过ActionResult<T>以下方式解决

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?添加如此基本的内容似乎需要大量工作。


我认为如果route-resource有效,但未返回任何内容,则正确的响应应为204(无内容)....如果路由不存在,则应返回404响应(未找到)。很有道理,对吧?
Hackerman'1

1
@Hackerman我怀疑这可能是一个选择,但是很多客户端API都可以推广(在其上1xx ... 2xx正确,3xx进入此处,4xx您理解错了,5xx我们理解错了)-2xx表示所有好的,当用户实际请求的资源不存在时(就像我问题中的示例一样,Fetch API认为204可以继续)。我想204可能意味着正确的资源路径,错误的资源ID,但这不是我的理解。有人将其作为所需的模式吗?
基思

1
看看这篇文章racksburg.com/choosing-an-http-status-code ...我认为状态码2xx / 3xx的流程图很好地解释了这一点...您也可以看看其他的:) \
Hackerman'1

@Hackerman在这种情况下,仍然指出404是正确的。
基思(Keith)

1
@Hackerman我真的不确定这一点-似乎试图在API中传达有关API的信息。如果这样做,我们是否也应该为没有请求的操作等的有效控制器实现405(而不是404)?在REST中,返回的是资源,而不是API本身。我认为这就是为什么约定名称要使用复数形式(而不是像在数据库中那样使用单数形式)的api/things/5原因,这是期望成为单一事物的资源。
基思

15

实际上,你可以使用IActionResultTask<IActionResult>代替Thing或者Task<Thing>甚至Task<IEnumerable<Thing>>。如果您有返回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

这似乎是使用IActionResultor的唯一缺点Task<IActionResult>。如果您确实确实想明确表示并且仍然想要设置状态代码,则有几种方法可以执行此操作-但由于该框架已经具有内置机制,因此人们对此并不满意。IActionResultController类中使用返回方法包装器。但是,您可以编写一些自定义中间件来处理此问题。

最后,我想指出的是,如果API调用null根据W3返回,则状态码204实际上是正确的。您到底为什么要404

204

服务器已完成请求,但不需要返回实体,可能要返回更新的元信息。响应可以以实体标题的形式包含新的或更新的元信息,如果存在,则应与所请求的变量相关联。

如果客户端是用户代理,则不应更改导致发送请求的文档视图。尽管任何新的或更新的元信息都应该应用于当前在用户代理的活动视图中的文档,但是该响应主要旨在允许输入操作而不会导致更改用户代理的活动文档视图。

204响应必须不包含消息正文,因此始终由标头字段之后的第一个空行终止。

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

服务器未找到与请求URI匹配的任何内容。没有迹象表明这种情况是暂时的还是永久的。如果服务器通过某种内部可配置的机制得知旧资源永久不可用并且没有转发地址,则应使用410(已消失)状态代码。当服务器不希望确切显示请求被拒绝的原因或没有其他响应可用时,通常使用此状态代码。

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


嗨,大卫,欢呼。我意识到我可以切换到返回IActionResult-但是如果这就是答案,那么如果真的需要IActionResult的话,为什么ASP.NET Core API Controller完全支持隐式类型转换?
基思

2
干杯大卫,但不是-该评论是我问题的直接引用。同样从我的问题出发,我不是在寻找收益NotFound();作为答案-这些仅与IActionResult...一起使用,这似乎使明确的收益类型变得毫无意义。那是我真正要问的,不是“我如何使用NotFound()”,而是“我应该如何”返回带有显式键入404的“-您的答案似乎是“不使用显式键入”,但如果是,则它缺少关于为何显式键入为默认(甚至完全受支持)
Keith

1
这应该是正确的答案...关于状态码,根据您要完成的工作以及404在某些情况下要使用...的情况,有不同的实现(有些使用一种代码,有些使用另一种代码)只要您的api有充分的文档记录,就不应该成为问题。
Hackerman'1

9
这不应该是正确的答案。如果要/ get / {id}并且服务器上没有该id的元素,则404-not found是正确的答案。关于显式键入-该方法的类型信息实际上在诸如swagger之类的工具中使用,该工具依赖于控制器声明能够生成正确的swagger文件。因此,在这种情况下,IActionResult缺少很多信息。
Sebastian PR Gingter '17

1
好东西,这没有被接受为正确的答案。感谢您的评论。:)
大卫·派恩

3

为了达到这个目的(不过,我认为,最好的办法应该使用IActionResult),你可以跟随,在那里你可以throwHttpResponseException,如果你的Thing就是null

// 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
Keith,

0

从控制器返回“显式类型”所要做的就是不配合您显式处理自己的响应代码的要求。最简单的解决方案是与IActionResult其他方法一起使用(就像其他人所建议的那样);不过,您也可以使用[Produces]过滤器。

使用 IActionResult

控制状态结果的方法是,您需要返回IActionResult,然后您就可以在其中利用StatusCodeResult类型。但是,现在您遇到了要强制使用特殊格式的问题...

下面的内容摘自Microsoft文档:格式化响应数据-强制使用特殊格式

强制特殊格式

如果您想限制特定操作的响应格式,则可以应用[Produces]过滤器。所述 [Produces]过滤器指定为特定动作(或控制器)的响应的格式。像大多数过滤器一样,它可以应用于操作,控制器或全局范围。

全部放在一起

StatusCodeResult除了控制“显式返回类型”之外,这是一个控制的示例。

// GET: api/authors/search?namelike=foo
[Produces("application/json")]
[HttpGet("Search")]
public IActionResult Search(string namelike)
{
    var result = _authorRepository.GetByNameSubstring(namelike);
    if (!result.Any())
    {
        return NotFound(namelike);
    }
    return Ok(result);
}

我不是这种设计模式的大力支持者,但是我已经将这些担忧放在了其他人问题的其他答案中。您将需要注意,[Produces]过滤器将要求您将其映射到适当的格式化程序/序列化器/显式类型。您可以查看此答案以获取更多想法,也可以查看此答案对ASP.NET Core Web API实施更精细的控制


干杯@Svek,这很有趣,但是有点切线。我真的不关心(无论如何是这个问题)可能使用的格式IActionResult,而是首先需要它。
基思
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.