asp.net-core - example - iactionresult return json
Devolviendo un 404 desde un controlador de API Core ASP.NET explícitamente escrito(no IActionResult) (4)
Los controladores de la API ASP.NET Core generalmente devuelven tipos explícitos (y lo hacen de forma predeterminada si creas un nuevo proyecto), algo como:
[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...
}
El problema es que el return null;
- Devuelve un HTTP 204
: éxito, sin contenido.
Esto es considerado por muchos componentes de Javascript del lado del cliente como un éxito, por lo que hay un código como:
const response = await fetch(''.../api/things/5'', {method: ''GET'' ...});
if(response.ok)
return await response.json(); // Error, no content!
Una búsqueda en línea (como esta pregunta y esta respuesta ) apunta a un return NotFound();
útil return NotFound();
métodos de extensión para el controlador, pero todos estos devuelven IActionResult
, que no es compatible con el tipo de retorno de mi Task<Thing>
. Ese patrón de diseño se ve así:
// 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);
}
Eso funciona, pero para usarlo, el tipo de retorno de GetAsync
debe cambiarse a la Task<IActionResult>
- la escritura explícita se pierde, y todos los tipos de retorno en el controlador tienen que cambiar (es decir, no usar la escritura explícita) o hay Será una mezcla donde algunas acciones tratan con tipos explícitos y otras. Además, las pruebas unitarias ahora deben hacer suposiciones sobre la serialización y deserializar explícitamente el contenido de IActionResult
donde antes tenían un tipo concreto.
Hay muchas maneras de evitar esto, pero parece ser una confusión confusa que podría diseñarse fácilmente, por lo que la pregunta real es: ¿cuál es la forma correcta que pretenden los diseñadores de ASP.NET Core?
Parece que las opciones posibles son:
- Tenga una combinación extraña (desordenada de probar) de tipos explícitos e
IActionResult
según el tipo esperado. - Olvídese de los tipos explícitos, no son realmente compatibles con Core MVC, siempre use
IActionResult
(en cuyo caso, ¿por qué están presentes?) - Escriba una implementación de
HttpResponseException
yHttpResponseException
comoArgumentOutOfRangeException
(vea esta respuesta para una implementación). Sin embargo, eso requiere el uso de excepciones para el flujo del programa, lo que generalmente es una mala idea y también está en desuso por parte del equipo MVC Core . - Escriba una implementación de
HttpNoContentOutputFormatter
que devuelva404
para las solicitudes GET. - ¿Algo más me estoy perdiendo en cómo se supone que funciona Core MVC?
- ¿O hay una razón por la que
204
es correcto y404
incorrecto para una solicitud GET fallida?
Todo esto implica compromisos y refactorización que pierden algo o agregan lo que parece ser una complejidad innecesaria en desacuerdo con el diseño de MVC Core. ¿Qué compromiso es el correcto y por qué?
Esto se trata en ASP.NET Core 2.1 con ActionResult<T>
:
public ActionResult<Thing> Get(int id) {
Thing thing = GetThingFromDB();
if (thing == null)
return NotFound();
return thing;
}
O incluso:
public ActionResult<Thing> Get(int id) =>
GetThingFromDB() ?? NotFound();
Actualizaré esta respuesta con más detalle una vez que la haya implementado.
Respuesta original
En ASP.NET Web API 5 hubo una HttpResponseException
(como lo señaló Hackerman ) pero se eliminó de Core y no hay middleware para manejarlo.
Creo que este cambio se debe a .NET Core: donde ASP.NET trata de hacer todo de forma inmediata, ASP.NET Core solo hace lo que usted le dice específicamente (lo cual es una parte importante de por qué es mucho más rápido y portátil). ).
No puedo encontrar una biblioteca existente que haga esto, así que la he escrito yo misma. Primero necesitamos una excepción personalizada para verificar:
public class StatusCodeException : Exception
{
public StatusCodeException(HttpStatusCode statusCode)
{
StatusCode = statusCode;
}
public HttpStatusCode StatusCode { get; set; }
}
Luego necesitamos un controlador RequestDelegate
que verifique la nueva excepción y la convierta al código de estado de respuesta 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();
}
}
}
Luego registramos este middleware en nuestro Startup.Configure
:
public class Startup
{
...
public void Configure(IApplicationBuilder app)
{
...
app.UseMiddleware<StatusCodeExceptionHandler>();
Por último, las acciones pueden lanzar la excepción del código de estado HTTP, mientras que siguen devolviendo un tipo explícito que puede ser fácilmente probado por unidades sin la conversión de IActionResult
public Thing Get(int id) {
Thing thing = GetThingFromDB();
if (thing == null)
throw new StatusCodeException(HttpStatusCode.NotFound);
return thing;
}
Esto mantiene los tipos explícitos para los valores de retorno y permite una fácil distinción entre resultados vacíos exitosos ( return null;
) y un error porque no se puede encontrar algo (lo considero como lanzar una ArgumentOutOfRangeException
).
Si bien esta es una solución al problema, todavía no responde a mi pregunta: los diseñadores de la API web compilan el soporte para tipos explícitos con la expectativa de que se usarán, agregando un manejo específico para el return null;
¿De modo que produciría un 204 en lugar de un 200, y luego no agregaría ninguna forma de lidiar con el 404? Parece mucho trabajo agregar algo tan básico.
Lo que está haciendo con la devolución de "Tipos explícitos" desde el controlador no va a cooperar con su requisito de tratar explícitamente su propio código de respuesta . La solución más sencilla es ir con IActionResult
(como han sugerido otros); Sin embargo, también puede controlar explícitamente su tipo de devolución utilizando el filtro [Produces]
.
Utilizando IActionResult
.
La forma de obtener control sobre los resultados del estado es que necesita devolver un IActionResult
que es donde puede aprovechar el tipo StatusCodeResult
. Sin embargo, ahora tiene su problema de querer forzar un formato particular ...
Lo siguiente se ha tomado del documento de Microsoft: Formato de los datos de respuesta: forzar un formato particular
Forzando un formato particular
Si desea restringir los formatos de respuesta para una acción específica que pueda, puede aplicar el filtro
[Produces]
. El filtro[Produces]
especifica los formatos de respuesta para una acción específica (o controlador). Como la mayoría de los Filters , esto se puede aplicar en la acción, el controlador o el ámbito global.
Poniendolo todo junto
Aquí hay un ejemplo de control sobre el StatusCodeResult
además del control sobre el "tipo de retorno explícito".
// 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);
}
No soy un gran partidario de este patrón de diseño, pero he puesto esas preocupaciones en algunas respuestas adicionales a las preguntas de otras personas. Deberá tener en cuenta que el filtro [Produces]
requerirá que lo asigne al formateador / serializador / tipo explícito apropiado. Puede ver esta respuesta para obtener más ideas o esta para implementar un control más granular sobre su API web central de ASP.NET .
Para lograr algo como eso (aún así, creo que el mejor enfoque debería ser usar IActionResult
), puede seguir, donde puede throw
una HttpResponseException
si su Thing
es null
:
// 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;
}
Puede usar IActionResult
o Task<IActionResult>
lugar de Thing
o Task<Thing>
o incluso Task<IEnumerable<Thing>>
. Si tiene una API que devuelve JSON , simplemente puede hacer lo siguiente:
[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)
{
}
}
Actualizar
Parece que la preocupación es que ser explícito en el retorno de una API es de alguna manera útil, mientras que es posible que sea explícito , de hecho no es muy útil. Si está escribiendo pruebas unitarias que ejercen el flujo de solicitud / respuesta, por lo general va a verificar la devolución sin procesar (que probablemente sería JSON , es decir, una cadena en C # ). Simplemente puede tomar la cadena devuelta y convertirla de nuevo al equivalente fuertemente tipado para comparaciones utilizando Assert
.
Este parece ser el único defecto con el uso de IActionResult
o Task<IActionResult>
. Si realmente quieres ser explícito y aún quieres establecer el código de estado, hay varias formas de hacerlo, pero está mal visto ya que el marco ya tiene un mecanismo incorporado para esto, es decir; utilizando las envolturas del método de retorno IActionResult
en la clase Controller
. Podrías escribir algún middleware personalizado para manejar esto como quieras, sin embargo.
Finalmente, me gustaría señalar que si una llamada a la API devuelve un null
según W3 , el código de estado de 204 es realmente exacto. ¿Por qué demonios quieres un 404 ?
204
El servidor ha cumplido con la solicitud pero no necesita devolver un cuerpo de entidad, y puede querer devolver información actualizada. La respuesta PUEDE incluir metainformación nueva o actualizada en forma de encabezados de entidad, que si están presentes DEBEN asociarse con la variante solicitada.
Si el cliente es un agente de usuario, NO DEBE cambiar su vista de documento desde la que causó el envío de la solicitud. Esta respuesta está destinada principalmente a permitir que se realicen acciones para que se realicen acciones sin que se produzca un cambio en la vista de documento activo del agente de usuario, aunque cualquier información nueva o actualizada DEBE aplicarse al documento que se encuentra actualmente en la vista activa del agente de usuario.
La respuesta 204 NO DEBE incluir un cuerpo de mensaje y, por lo tanto, siempre termina con la primera línea vacía después de los campos de encabezado.
Creo que la primera oración del segundo párrafo lo dice mejor: "Si el cliente es un agente de usuario, NO DEBE cambiar su vista de documento desde la que causó el envío de la solicitud". Este es el caso con una API. En comparación con un 404 :
El servidor no ha encontrado nada que coincida con el URI de solicitud. No se da ninguna indicación de si la condición es temporal o permanente. El código de estado 410 (Desaparecido) DEBERÍA usarse si el servidor sabe, a través de algún mecanismo configurable internamente, que un recurso antiguo no está disponible permanentemente y no tiene una dirección de reenvío. Este código de estado se usa comúnmente cuando el servidor no desea revelar exactamente por qué se rechazó la solicitud o cuando no se aplica ninguna otra respuesta.
La diferencia principal es que una es más aplicable para una API y la otra para la vista de documento, es decir; la página mostrada.