ASP.NET Core API Controller 通常返回显式类型(如果创建新项目,则默认情况下会这样做),例如:

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

有很多方法可以解决此问题,但这似乎是一个容易混淆的困惑,因此真正的问题是: ASP.NET Core设计人员打算使用的正确方法是什么?

似乎可能的选择是:
  • 根据期望的类型,将显式类型和IActionResult混合使用(测试很麻烦)。
  • 忘记显式类型,Core MVC并不真正支持它们,请始终使用IActionResult(在这种情况下为什么要显示它们?)
  • 编写HttpResponseException的实现,并将其像ArgumentOutOfRangeException一样使用(有关实现,请参见this answer)。但是,这确实需要对程序流使用异常,这通常是一个坏主意,也是deprecated by the MVC Core team
  • 编写HttpNoContentOutputFormatter的实现,该实现为GET请求返回404
  • Core MVC应该如何工作,我还缺少其他东西?
  • 还是由于失败的GET请求而导致204正确而404错误的原因?

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

    最佳答案

    这是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指出),但是它已从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>();
    

    最后, Action 可以引发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?添加如此基本的内容似乎需要大量工作。

    08-04 15:23