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 question 和 this answer )指向有用的 return NotFound();
Controller 的扩展方法,但所有这些都返回 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 上的所有返回类型都必须更改(即根本不使用显式类型)或者将混合在一起,其中一些操作处理显式类型而其他操作。此外,单元测试现在需要对序列化做出假设并显式反序列化 IActionResult
的内容。之前他们有一个具体的类型。
有很多方法可以解决这个问题,但它似乎是一个很容易设计出来的令人困惑的大杂烩,所以真正的问题是:ASP.NET Core 设计者打算采用的正确方法是什么?强>
似乎可能的选择是:
- 将显式类型和
IActionResult
奇怪地(难以测试)混合在一起取决于预期的类型。 - 忘记显式类型,Core MVC 并不真正支持它们,始终使用
IActionResult
(在这种情况下,他们为什么会出现?) - 编写
HttpResponseException
的实现并像ArgumentOutOfRangeException
一样使用它(参见 this answer 的实现)。然而,这确实需要为程序流使用异常,这通常是一个坏主意,也是 deprecated by the MVC Core team . - 编写
HttpNoContentOutputFormatter
的实现返回404
对于 GET 请求。 - 在 Core MVC 应该如何工作方面我还遗漏了什么?
- 或者有什么原因
204
是正确的404
失败的 GET 请求错误?
这些都涉及妥协和重构,会丢失一些东西或添加一些看似不必要的复杂性,这与 MVC Core 的设计不一致。哪种折衷方案是正确的,为什么?
最佳答案
这是 addressed in ASP.NET Core 2.1与 ActionResult<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>();
最后,操作可以抛出 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 的方法?添加如此基本的东西似乎需要做很多工作。
关于c# - 从显式类型化的 ASP.NET Core API Controller (不是 IActionResult)返回 404,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41464540/