[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... }
问题是返回null; – 它返回HTTP 204:成功,没有内容。
然后,许多客户端Javascript组件认为这是成功的,因此有如下代码:
const response = await fetch('.../api/things/5',{method: 'GET' ...}); if(response.ok) return await response.json(); // Error,no content!
在线搜索(例如this question和this answer)指向有用的返回NotFound();控制器的扩展方法,但所有这些都返回IActionResult,它与我的任务< 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核心设计师的正确方法是什么?
似乎可能的选择是:
>根据预期的类型,有一个奇怪的(混乱的测试)显式类型和IActionResult混合。
>忘记显式类型,Core MVC并不真正支持它们,总是使用IActionResult(在这种情况下它们为什么会出现?)
>编写HttpResponseException的实现并将其用作ArgumentOutOfRangeException(有关实现,请参阅this answer)。但是,这确实需要使用程序流的例外,这通常是一个坏主意,也是deprecated by the MVC Core team。
>编写HttpNoContentOutputFormatter的实现,为GET请求返回404。
>我还缺少核心MVC应该如何工作的其他东西?
>或者有一个理由为什么204是正确的,404错误的GET请求失败?
这些都涉及妥协和重构,这些都会丢失一些东西,或者增加与MVC Core设计相悖的不必要的复杂性。哪个妥协是正确的,为什么?
解决方法
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.Infigure中注册这个中间件:
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; }
这保留了返回值的显式类型,并允许轻松区分成功的空结果(返回null;)和错误,因为无法找到某些内容(我认为它就像抛出ArgumentOutOfRangeException)。
虽然这是问题的解决方案,但它仍然没有真正回答我的问题 – Web API的设计者构建了对显式类型的支持,期望它们将被使用,添加了返回null的特定处理;这样它会产生204而不是200,然后没有添加任何方式来处理404?添加如此基本的东西似乎需要付出很多努力。