首页 文章

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

提问于
浏览
41

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 的内容 .

有很多方法可以解决这个问题,但它似乎很容易被设计出来,所以真正的问题是: what is the correct way intended by the ASP.NET Core designers?

似乎可能的选择是:

  • 根据预期的类型,有一个奇怪的(混乱的测试)混合的显式类型和 IActionResult .

  • 忘记显式类型,Core MVC并不真正支持它们,总是使用 IActionResult (在这种情况下它们为什么会出现?)

  • 编写 HttpResponseException 的实现并将其用作 ArgumentOutOfRangeException (有关实现,请参阅this answer) . 但是,这确实需要使用程序流的异常,这通常是一个坏主意,也是deprecated by the MVC Core team .

  • 编写 HttpNoContentOutputFormatter 的实现,为GET请求返回 404 .

  • 我还缺少核心MVC的工作原理吗?

  • 或者 204 是否正确且_2599973_错误导致GET请求失败?

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

4 回答

  • 0

    这是addressed in ASP.NET Core 2.1ActionResult<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指出)但它没有中间件来处理它 .

    我认为这种变化是由.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?添加如此基本的东西似乎需要付出很多努力 .

  • 3

    您实际上可以使用 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)
        {
        }
    }
    

    Update

    似乎关注的是,在API的返回中显式是有帮助的,而有可能是明确的,它实际上并不是非常有用 . 如果您正在编写执行请求/响应管道的单元测试,您通常会验证原始返回(最有可能是JSON,即 . 字符串 C# ) . 您可以简单地获取返回的字符串并将其转换回强类型的等效项,以便使用 Assert 进行比较 .

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

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

    204

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

    我认为第二段的第一句话说得最好,"If the client is a user agent, it SHOULD NOT change its document view from that which caused the request to be sent" . 这是API的情况 . 与 404 相比:

    服务器未找到与Request-URI匹配的任何内容 . 没有说明该病症是暂时的还是永久性的 . 如果服务器通过一些内部可配置的机制知道旧资源永久不可用且没有转发地址,则应该使用410(Gone)状态代码 . 当服务器不希望确切地说明请求被拒绝的原因,或者没有其他响应适用时,通常会使用此状态代码 .

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

  • 14

    为了完成这样的事情(仍然,我认为最好的方法应该是使用 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;
    }
    
  • 41

    你从控制器返回 "Explicit Types" 所做的是不能配合你明确处理自己的 response code 的要求 . 最简单的解决方案是使用 IActionResult (与其他人建议的那样);但是,您也可以使用 [Produces] 过滤器显式控制返回类型 .

    Using IActionResult.

    控制状态结果的方法是,您需要返回一个 IActionResult ,然后您可以使用 StatusCodeResult 类型 . 但是,现在您已经遇到了想要强制执行部分格式的问题......

    以下内容取自Microsoft文档:Formatting Response Data -Forcing a Particular Format

    强制使用特定格式如果要限制特定操作的响应格式,可以应用[Produces]过滤器 . [Produces]过滤器指定特定操作(或控制器)的响应格式 . 与大多数过滤器一样,这可以应用于操作,控制器或全局范围 .

    把它们放在一起

    除了控制"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);
    }
    

    我'm not a big supporter of this design pattern, but I have put those concerns in some additional answers to other people'的问题 . 您需要注意 [Produces] 过滤器将要求您将其映射到相应的Formatter / Serializer / Explicit Type . 您可以查看this answer以获取更多创意,或者将此视图用于implementing a more granular control over your ASP.NET Core Web API .

相关问题