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 question 和 this 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
的内容之前明确反序列化它们具有具体类型。
有很多方法可以解决这个问题,但它似乎是一个很容易设计出来的令人困惑的混搭,所以真正的问题是:ASP.NET Core 设计者想要的正确方法是什么?
似乎可能的选择是:
根据预期的类型,有一个奇怪的(测试混乱的)显式类型和 IActionResult 组合。忘记显式类型,Core MVC 并不真正支持它们,总是使用 IActionResult (在这种情况下为什么它们会出现?)编写 HttpResponseException 的实现并像 ArgumentOutOfRangeException 一样使用它(参见这个答案的实现)。但是,这确实需要对程序流使用异常,这通常是一个坏主意,并且 MVC Core 团队也不赞成使用。编写一个 HttpNoContentOutputFormatter 的实现,它为 GET 请求返回 404。我在 Core MVC 的工作方式中还缺少什么?或者对于失败的 GET 请求,为什么 204 正确而 404 错误是有原因的?
这些都涉及妥协和重构,它们会丢失一些东西或增加一些似乎不必要的复杂性,与 MVC Core 的设计相悖。哪种妥协是正确的,为什么?
StatusCode(500)
,它仅适用于返回 IActionResult
的操作,然后我将详细介绍。
IActionResult
。我问的是显式类型 的操作。我继续询问第一个要点中 IActionResult
的使用,但我不是在问如何调用 StatusCode(404)
- 我已经知道并在问题中引用它。
return new HttpResponseMessage(HttpStatusCode.NotFound);
...也根据此:docs.microsoft.com/en-us/aspnet/core/mvc/models/formatting For non-trivial actions with multiple return types or options (for example, different HTTP status codes based on the result of operations performed), prefer IActionResult as the return type.
IActionResult
),您可以按照此示例 public Item Get(int id) { var item = _repo.FindById(id); if (item == null) throw new HttpResponseException(HttpStatusCode.NotFound); return item; }
进行操作,您可以在其中返回 {3 } 如果 thing
是 null
...
这是带有 ActionResult<T>
的 addressed in ASP.NET Core 2.1:
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>();
finally 操作可以抛出 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的方法?添加如此基本的东西似乎需要做很多工作。
您实际上可以使用 IActionResult
或 Task<IActionResult>
代替 Thing
或 Task<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)
{
}
}
更新
似乎担心的是,在返回 API 时 explicit 在某种程度上是有帮助的,虽然可能是 explicit 它实际上并不是很有用。如果您正在编写执行请求/响应管道的单元测试,您通常会验证原始返回(很可能是 JSON,即;C#)。您可以简单地获取返回的字符串并将其转换回强类型等效项,以便使用 Assert
进行比较。
这似乎是使用 IActionResult
或 Task<IActionResult>
的唯一缺点。如果你真的,真的想要明确并且仍然想设置状态码,有几种方法可以做到这一点 - 但它不赞成,因为框架已经有一个内置机制,即;使用 Controller
类中的 IActionResult
返回方法包装器。不过,您可以编写一些 custom middleware 来处理这个问题。
最后,我想指出,如果 API 调用根据 W3 返回 null
,则状态码 204 实际上是准确的。为什么你会想要一个 404?
204
服务器已完成请求,但不需要返回实体主体,并且可能希望返回更新的元信息。响应可能包括实体头形式的新的或更新的元信息,如果存在,应该与请求的变体相关联。如果客户端是用户代理,它不应该改变导致请求被发送的文档视图。这个响应主要是为了允许输入动作发生而不导致用户代理的活动文档视图发生变化,尽管任何新的或更新的元信息应该应用于当前在用户代理的活动视图中的文档。 204 响应不能包含消息体,因此总是由头字段之后的第一个空行终止。
我认为第二段的第一句话说得最好,“如果客户端是用户代理,它不应该改变导致发送请求的文档视图”。 API 就是这种情况。与 404 相比:
服务器未找到任何与请求 URI 匹配的内容。没有说明这种情况是暂时的还是永久性的。如果服务器通过一些内部可配置的机制知道旧资源永久不可用并且没有转发地址,则应该使用 410 (Gone) 状态代码。当服务器不希望确切地揭示请求被拒绝的原因或没有其他响应适用时,通常使用此状态代码。
主要区别是一个更适用于 API,另一个更适用于文档视图,即;显示的页面。
IActionResult
- 但如果这是答案,为什么 ASP.NET Core API 控制器在真正需要 IActionResult 时完全支持隐式类型转换?
NotFound();
作为答案 - 这些仅适用于 IActionResult
...这似乎会使显式返回类型毫无意义这就是我真正要问的,而不是“我如何使用 NotFound()
”,而是“我应该如何返回 404 显式输入” - 你的答案似乎是“不要使用显式输入”,但如果是的话,那就是缺少关于为什么显式类型是默认(甚至完全支持)的关键细节
404
在某些情况下,只要您的 api 有据可查,这应该不是问题。
为了完成类似的事情(不过,我认为最好的方法应该是使用 IActionResult
),您可以按照,如果您的 Thing
是 null
,您可以在其中 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;
}
HttpResponseException
和处理它的中间件似乎不在 .NET Core 1.1 中。下一个问题是:我应该推出自己的中间件还是现有的(理想情况下是 MS)包已经这样做了?
Startup.Configure
中添加一个新的可选中间件,但这里似乎没有。取而代之的是,从头开始编写一个似乎并不难。
当我想为发送到请求中的错误数据返回 400 响应时,我也想知道如何处理强类型响应的答案。我的项目在 ASP.NET Core Web API (.NET5.0) 中。我找到的解决方案基本上是设置状态码并返回对象的 default 版本。这是您的示例,其中将状态代码设置为 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...
}
具有相同行为的另一个问题 - 所有方法都返回 404。问题在于缺少代码块
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
不定期副业成功案例分享
api/things/5
是期望成为单个 事物 的资源。