ChatGPT解决这个技术问题 Extra ChatGPT

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

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<Thing> 返回类型不兼容。该设计模式如下所示:

// 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 的内容之前明确反序列化它们具有具体类型。

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

似乎可能的选择是:

根据预期的类型,有一个奇怪的(测试混乱的)显式类型和 IActionResult 组合。忘记显式类型,Core MVC 并不真正支持它们,总是使用 IActionResult (在这种情况下为什么它们会出现?)编写 HttpResponseException 的实现并像 ArgumentOutOfRangeException 一样使用它(参见这个答案的实现)。但是,这确实需要对程序流使用异常,这通常是一个坏主意,并且 MVC Core 团队也不赞成使用。编写一个 HttpNoContentOutputFormatter 的实现,它为 GET 请求返回 404。我在 Core MVC 的工作方式中还缺少什么?或者对于失败的 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),您可以按照此示例 public Item Get(int id) { var item = _repo.FindById(id); if (item == null) throw new HttpResponseException(HttpStatusCode.NotFound); return item; } 进行操作,您可以在其中返回 {3 } 如果 thingnull...

K
Keith

这是带有 ActionResult<T>addressed in ASP.NET Core 2.1

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>();

finally 操作可以抛出 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(无内容)......如果路由不存在应该返回 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 本身。我认为这就是为什么约定名称为复数(而不是像它们在 DB 中那样的单数)的原因 - api/things/5 是期望成为单个 事物 的资源。
D
David Pine

您实际上可以使用 IActionResultTask<IActionResult> 代替 ThingTask<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 时 explicit 在某种程度上是有帮助的,虽然可能是 explicit 它实际上并不是很有用。如果您正在编写执行请求/响应管道的单元测试,您通常会验证原始返回(很可能是 JSON,即;C#)。您可以简单地获取返回的字符串并将其转换回强类型等效项,以便使用 Assert 进行比较。

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

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

204

服务器已完成请求,但不需要返回实体主体,并且可能希望返回更新的元信息。响应可能包括实体头形式的新的或更新的元信息,如果存在,应该与请求的变体相关联。如果客户端是用户代理,它不应该改变导致请求被发送的文档视图。这个响应主要是为了允许输入动作发生而不导致用户代理的活动文档视图发生变化,尽管任何新的或更新的元信息应该应用于当前在用户代理的活动视图中的文档。 204 响应不能包含消息体,因此总是由头字段之后的第一个空行终止。

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

服务器未找到任何与请求 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 在这种情况下会丢失很多信息。
好在这没有被接受为正确的答案。感谢您的评论。 :)
H
Hackerman

为了完成类似的事情(不过,我认为最好的方法应该是使用 IActionResult),您可以按照,如果您的 Thingnull,您可以在其中 throw 一个 HttpResponseException

// 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
J
J Man

当我想为发送到请求中的错误数据返回 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...
}

V
Vlad

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

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