简体   繁体   English

从显式类型的 ASP.NET 核心返回 404 API controller(不是 IActionResult)

[英]Returning a 404 from an explicitly typed ASP.NET Core API controller (not IActionResult)

ASP.NET Core API controllers typically return explicit types (and do so by default if you create a new project), something like: ASP.NET 核心 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...
}

The problem is that return null;问题是return null; - it returns an HTTP 204 : success, no content. - 它返回 HTTP 204 :成功,无内容。

This is then regarded by a lot of client side Javascript components as success, so there's code like:这被很多客户端 Javascript 组件视为成功,所以有这样的代码:

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

A search online (such as this question and this answer ) points to helpful return NotFound();在线搜索(例如这个问题这个答案)指向有用的return NotFound(); extension methods for the controller, but all these return IActionResult , which isn't compatible with my Task<Thing> return type. controller 的扩展方法,但所有这些方法都返回IActionResult ,这与我的Task<Thing>返回类型不兼容。 That design pattern looks like this:该设计模式如下所示:

// 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);
}

That works, but to use it the return type of GetAsync must be changed to Task<IActionResult> - the explicit typing is lost, and either all the return types on the controller have to change (ie not use explicit typing at all) or there will be a mix where some actions deal with explicit types while others.这可行,但要使用它,必须将GetAsync的返回类型更改为Task<IActionResult> - 显式类型丢失,并且 controller 上的所有返回类型都必须更改(即根本不使用显式类型)或那里将是一种混合,其中一些操作处理显式类型,而另一些操作。 In addition unit tests now need to make assumptions about the serialisation and explicitly deserialise the content of the IActionResult where before they had a concrete type.此外,单元测试现在需要对序列化做出假设,并在它们具有具体类型之前显式反序列化IActionResult的内容。

There are loads of ways around this, but it appears to be a confusing mishmash that could easily be designed out, so the real question is: what is the correct way intended by the ASP.NET Core designers?有很多方法可以解决这个问题,但它似乎是一个很容易设计出来的令人困惑的大杂烩,所以真正的问题是: ASP.NET Core 设计者打算采用的正确方法是什么?

It seems that the possible options are:似乎可能的选择是:

  1. Have a weird (messy to test) mix of explicit types and IActionResult depending on expected type.有一个奇怪的(测试混乱)显式类型和IActionResult的混合,具体取决于预期类型。
  2. Forget about explicit types, they're not really supported by Core MVC, always use IActionResult (in which case why are they present at all?)忘掉显式类型,Core MVC 并不真正支持它们,总是使用IActionResult (在这种情况下,它们为什么会出现?)
  3. Write an implementation of HttpResponseException and use it like ArgumentOutOfRangeException (see this answer for an implementation).编写HttpResponseException的实现并像ArgumentOutOfRangeException一样使用它(有关实现,请参见此答案)。 However, that does require using exceptions for program flow, which is generally a bad idea and also deprecated by the MVC Core team .然而,这确实需要为程序流使用异常,这通常是一个坏主意,并且也被 MVC 核心团队弃用
  4. Write an implementation of HttpNoContentOutputFormatter that returns 404 for GET requests.编写一个HttpNoContentOutputFormatter的实现,它为 GET 请求返回404
  5. Something else I'm missing in how Core MVC is supposed to work?我在 Core MVC 应该如何工作方面还缺少其他东西?
  6. Or is there a reason why 204 is correct and 404 wrong for a failed GET request?还是对于失败的 GET 请求, 204是正确的而404是错误的?

These all involve compromises and refactoring that lose something or add what seems to be unnecessary complexity at odds with the design of MVC Core.这些都涉及妥协和重构,会丢失一些东西或添加一些看似不必要的复杂性,这与 MVC Core 的设计不一致。 Which compromise is the correct one and why?哪一种妥协是正确的,为什么?

This is addressed in ASP.NET Core 2.1 with ActionResult<T> :在 ASP.NET Core 2.1 中通过ActionResult<T>

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

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

    return thing;
}

Or even:甚至:

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

I'll update this answer with more detail once I've implemented it.实施后,我将更详细地更新此答案。

Original Answer原答案

In ASP.NET Web API 5 there was an HttpResponseException (as pointed out by Hackerman ) but it's been removed from Core and there's no middleware to handle it.在 ASP.NET Web API 5 中有一个HttpResponseException (正如Hackerman指出的),但它已从 Core 中删除,并且没有中间件来处理它。

I think this change is due to .NET Core - where ASP.NET tries to do everything out of the box, ASP.NET Core only does what you specifically tell it to (which is a big part of why it's so much quicker and portable).我认为这种变化是由于 .NET Core - ASP.NET 尝试开箱即用地做所有事情,ASP.NET Core 只做你特别告诉它的事情(这是为什么它如此快速和可移植的重要原因) )。

I can't find a an existing library that does this, so I've written it myself.我找不到可以执行此操作的现有库,因此我自己编写了它。 First we need a custom exception to check for:首先,我们需要一个自定义异常来检查:

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

    public HttpStatusCode StatusCode { get; set; }
}

Then we need a RequestDelegate handler that checks for the new exception and converts it to the HTTP response status code:然后我们需要一个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();
        }
    }
}

Then we register this middleware in our Startup.Configure :然后我们在Startup.Configure注册这个中间件:

public class Startup
{
    ...

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

Finally actions can throw the HTTP status code exception, while still returning an explicit type that can easily be unit tested without conversion from IActionResult :最后,操作可以抛出 HTTP 状态代码异常,同时仍然返回一个显式类型,无需从IActionResult转换即可轻松进行单元测试:

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

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

    return thing;
}

This keeps the explicit types for the return values and allows easy distinction between successful empty results ( return null; ) and an error because something can't be found (I think of it like throwing an ArgumentOutOfRangeException ).这保留了返回值的显式类型,并允许轻松区分成功的空结果( return null; )和错误,因为找不到某些东西(我认为它就像抛出ArgumentOutOfRangeException )。

While this is a solution to the problem it still doesn't really answer my question - the designers of the Web API build support for explicit types with the expectation that they would be used, added specific handling for return null;虽然这是问题的解决方案,但它仍然没有真正回答我的问题——Web API 的设计者构建了对显式类型的支持,期望它们会被使用,添加了对return null;特定处理return null; so that it would produce a 204 rather than a 200, and then didn't add any way to deal with 404?这样它会产生204而不是200,然后没有添加任何方法来处理404? It seems like a lot of work to add something so basic.添加如此基本的东西似乎需要做很多工作。

You can actually use IActionResult or Task<IActionResult> instead of Thing or Task<Thing> or even Task<IEnumerable<Thing>> .您实际上可以使用IActionResultTask<IActionResult>而不是ThingTask<Thing>甚至Task<IEnumerable<Thing>> If you have an API that returns JSON then you can simply do the following:如果您有一个返回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)
    {
    }
}

Update更新

It seems as though the concern is that being explicit in the return of an API is somehow helpful, while it is possible to be explicit it is in fact not very useful.似乎令人担忧的是,在 API 的返回中显式在某种程度上是有帮助的,虽然可能显式但实际上并不是很有用。 If you're writing unit tests that exercise the request / response pipeline you are typically going to verify the raw return (which would most likely be JSON , ie; a string in C# ).如果您正在编写执行请求/响应管道的单元测试,您通常会验证原始返回(最有可能是JSON ,即C# 中的字符串)。 You could simply take the returned string and convert it back to the strongly typed equivalent for comparisons using Assert .您可以简单地获取返回的字符串并将其转换回强类型等效项以使用Assert进行比较。

This seems to be the only shortcoming with using IActionResult or Task<IActionResult> .这似乎是使用IActionResultTask<IActionResult>的唯一缺点。 If you really, really want to be explicit and still want to set the status code there are several ways to do this - but it is frowned upon as the framework already has a built-in mechanism for this, ie;如果你真的,真的想要明确并且仍然想要设置状态代码,有几种方法可以做到这一点 - 但它是不赞成的,因为框架已经有一个内置的机制,即; using the IActionResult returning method wrappers in the Controller class.Controller类中使用IActionResult返回方法包装器。 You could write some custom middleware to handle this however you'd like, however.但是,您可以编写一些自定义中间件来处理此问题。

Finally, I would like to point out that if an API call returns null according to W3 a status code of 204 is actually accurate.最后,我想指出,如果 API 调用根据W3返回null ,则状态代码204实际上是准确的。 Why on earth would you want a 404 ?你到底为什么想要一个404

204 204

The server has fulfilled the request but does not need to return an entity-body, and might want to return updated metainformation.服务器已完成请求但不需要返回实体主体,并且可能想要返回更新的元信息。 The response MAY include new or updated metainformation in the form of entity-headers, which if present SHOULD be associated with the requested variant.响应可以包含实体头形式的新的或更新的元信息,如果存在,应该与请求的变体相关联。

If the client is a user agent, it SHOULD NOT change its document view from that which caused the request to be sent.如果客户端是一个用户代理,它不应该改变导致请求被发送的文档视图。 This response is primarily intended to allow input for actions to take place without causing a change to the user agent's active document view, although any new or updated metainformation SHOULD be applied to the document currently in the user agent's active view.该响应主要是为了允许输入操作发生,而不会导致用户代理的活动文档视图发生变化,尽管任何新的或更新的元信息都应该应用于当前在用户代理的活动视图中的文档。

The 204 response MUST NOT include a message-body, and thus is always terminated by the first empty line after the header fields. 204 响应不能包含消息体,因此总是由头字段之后的第一个空行终止。

I think the first sentence of the second paragraph says it best, "If the client is a user agent, it SHOULD NOT change its document view from that which caused the request to be sent".我认为第二段的第一句话说得最好,“如果客户端是用户代理,它不应该从导致发送请求的文档视图中更改其文档视图”。 This is the case with an API. API 就是这种情况。 As compared to a 404 :404相比:

The server has not found anything matching the Request-URI.服务器未找到任何与请求 URI 匹配的内容。 No indication is given of whether the condition is temporary or permanent.没有说明这种情况是暂时的还是永久性的。 The 410 (Gone) status code SHOULD be used if the server knows, through some internally configurable mechanism, that an old resource is permanently unavailable and has no forwarding address.如果服务器通过某些内部可配置机制知道旧资源永久不可用且没有转发地址,则应使用 410(消失)状态代码。 This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable.当服务器不想透露请求被拒绝的确切原因时,或者当没有其他响应适用时,通常使用此状态代码。

The primary difference being one is more applicable for an API and the other for the document view, ie;主要区别是一个更适用于 API,另一个更适用于文档视图,即; the page displayed.显示的页面。

In order to accomplish something like that(still, I think that the best approach should be using IActionResult ), you can follow, where you can throw an HttpResponseException if your Thing is null :为了完成类似的事情(仍然,我认为最好的方法应该是使用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;
}

From ASP.NET Core 7, a action controller can return a HttpResults type.从 ASP.NET Core 7 开始,action controller 可以返回一个 HttpResults 类型。 Then you can:那么你就可以:

public async Task<Results<Ok<Product>, NotFound>> GetAsync(int id)
{
    Thing thingFromDB = await GetThingFromDBAsync();
    if(thingFromDB == null)
        return TypedResults.NotFound();

    ...
    return TypedResults.Ok(thingFromDB);
}

I love this syntax, because it's explicitly indicate that return the API. But actually, the openAPI specification generator don't manage this.我喜欢这种语法,因为它明确指示返回 API。但实际上,openAPI 规范生成器不管理它。 You can follow the progress from this Github ticket:您可以从这张 Github 工单关注进度:

TypedResults metadata are not inferred for API Controllers不为 API 控制器推断 TypedResults 元数据

What you're doing with returning "Explicit Types" from the controller is isn't going to cooperate with your requirement to explicitly deal with your own response code .您从控制器返回“显式类型”所做的工作不会与您的要求合作,以明确处理您自己的响应代码 The simplest solution is to go with IActionResult (like others have suggested);最简单的解决方案是使用IActionResult (就像其他人建议的那样); However, you can also explicitly control your return type using the [Produces] filter.但是,您也可以使用[Produces]过滤器显式控制您的返回类型。

Using IActionResult .使用IActionResult

The way to get control over the status results, is you need to return a IActionResult which is where you can then take advantage of the StatusCodeResult type.控制状态结果的方法是,您需要返回一个IActionResult ,然后您可以在其中利用StatusCodeResult类型。 However, now you've got your issue of wanting to force a partiular format...但是,现在您遇到了想要强制特定格式的问题......

The stuff below is taken from the Microsoft Document: Formatting Response Data -Forcing a Particular Format以下内容摘自 Microsoft 文档: 格式化响应数据 - 强制特定格式

Forcing a Particular Format强制特定格式

If you would like to restrict the response formats for a specific action you can, you can apply the [Produces] filter.如果您想限制特定操作的响应格式,您可以应用[Produces]过滤器。 The [Produces] filter specifies the response formats for a specific action (or controller). [Produces]过滤器指定特定操作(或控制器)的响应格式。 Like most Filters , this can be applied at the action, controller, or global scope.像大多数Filters一样,这可以应用于操作、控制器或全局范围。

Putting it all together把这一切放在一起

Here's an example of control over the StatusCodeResult in addition to control over the "explicit return type".除了控制“显式返回类型”之外,这里还有一个控制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);
}

I'm not a big supporter of this design pattern, but I have put those concerns in some additional answers to other people's questions.我不是这种设计模式的大力支持者,但我已经将这些担忧放在了其他人问题的一些额外答案中。 You will need to note that the [Produces] filter will require you to map it to the appropriate Formatter / Serializer / Explicit Type.您需要注意[Produces]过滤器将要求您将其映射到适当的格式化程序/序列化程序/显式类型。 You could take a look at this answer for more ideas or this one for implementing a more granular control over your ASP.NET Core Web API .您可以查看此答案以获取更多想法,或者查看此答案实现对 ASP.NET Core Web API 的更精细控制

Had another problem with same behavior - all methods return 404. The problem was in missing code block有另一个相同行为的问题 - 所有方法都返回 404。问题在于缺少代码块

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

I too looked high and low for an answer to what to do about strongly typed responses when I wanted to return an 400 response for bad data sent into the request.当我想为发送到请求中的错误数据返回 400 响应时,我也看了很多关于如何处理强类型响应的答案。 My project is in ASP.NET Core Web API (.NET5.0).我的项目在 ASP.NET Core Web API (.NET5.0) 中。 The solution I found was basically set the status code and return default version of the object.我找到的解决方案基本上是设置状态代码并返回对象的默认版本。 Here is your example with the change to set the status code to 404 and return the default object when the db object is null.这是您将状态代码设置为 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...
}

ASP.NET Core 3.1 introduced filter . ASP.NET Core 3.1 引入了过滤器

Filters in ASP.NET Core allow code to run before or after specific stages in the request processing pipeline. ASP.NET Core 中的过滤器允许代码在请求处理管道的特定阶段之前或之后运行。

You can define a result filter that transform null ok result to not found result:您可以定义一个结果过滤器,将 null ok 结果转换为 not found 结果:

public class NullAsNotFoundResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    { }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        if(context.Result is ObjectResult result && result.Value == null)
        {
            context.Result = new NotFoundResult();
        }
    }
}

Finally, you need to add the filter in the MVC pipeline:最后,您需要在 MVC 管道中添加过滤器:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(o => o.Filters.Add<NullAsNotFoundResultFilter>());

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 返回“JsonResult / IActionResult”或“Task”有什么不同 <SomeObject> “或者只是Asp.net Core Web API中的”SomeObject“? - What is the difference in returning “JsonResult/IActionResult” or “Task<SomeObject>” or just “SomeObject” in a Asp.net Core Web API? ASP.NET Core Web Api 控制器端点都返回 404 错误 - ASP.NET Core Web Api controller end points are all returning 404 error 从ASP.NET Core中的WebSocket请求返回什么IActionResult? - What IActionResult to return from a WebSocket request in ASP.NET Core? 未找到返回 404 的 Asp.Net Core“Web API”路由 - Asp.Net Core "Web API" Routes Returning 404 Not Found 尝试从 ASP.NET Core 5 Web 应用程序运行 Api 控制器功能时找不到 404 - 404 not found when trying to run Api Controller function from ASP.NET core 5 web app 如何在ASP.NET Core中返回自定义HTTP状态/消息而不返回对象,IActionResult等? - How to return a custom HTTP status/message in ASP.NET Core without returning object, IActionResult, etc? 从ASP.NET Core 2控制器返回PDF - Returning a PDF from an ASP.NET Core 2 Controller 返回 IAsyncEnumerable<T> 和来自 Asp.Net Core 控制器的 NotFound - Returning IAsyncEnumerable<T> and NotFound from Asp.Net Core Controller 如何测试我的 ASP.NET Core 2.2 Web API GET IActionResult 返回 Ok(object)? - How to test my ASP.NET Core 2.2 Web API GET IActionResult that returns Ok(object)? 如何将此IHttpActionResult转换为ASP.NET Core IActionResult - How to translate this IHttpActionResult to ASP.NET Core IActionResult
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM