¿Cómo devuelvo NotFound () IHttpActionResult con un mensaje de error o una excepción?

98

Estoy devolviendo un NotFound IHttpActionResult, cuando no se encuentra algo en mi acción WebApi GET. Junto con esta respuesta, quiero enviar un mensaje personalizado y / o el mensaje de excepción (si corresponde). La corriente ApiController's NotFound()método no proporciona una sobrecarga para pasar un mensaje.

¿Hay alguna forma de hacer esto? o tendré que escribir mi propia costumbre IHttpActionResult?

Ajay Jadhav
fuente
¿Desea devolver el mismo mensaje para todos los resultados No encontrados?
Nikolai Samteladze
@NikolaiSamteladze No, podría ser un mensaje diferente dependiendo de la situación.
Ajay Jadhav

Respuestas:

84

Debería escribir el resultado de su propia acción si desea personalizar la forma del mensaje de respuesta.

Queríamos proporcionar las formas de mensaje de respuesta más comunes listas para usar para cosas como simples 404 vacíos, pero también queríamos mantener estos resultados lo más simples posible; Una de las principales ventajas de utilizar los resultados de la acción es que hace que su método de acción sea mucho más fácil de realizar pruebas unitarias. Cuantas más propiedades pongamos en los resultados de la acción, más cosas debe tener en cuenta la prueba unitaria para asegurarse de que el método de acción está haciendo lo que usted espera.

A menudo también quiero la capacidad de proporcionar un mensaje personalizado, así que no dude en registrar un error para que consideremos respaldar ese resultado de acción en una versión futura: https://aspnetwebstack.codeplex.com/workitem/list/advanced

Sin embargo, una cosa buena acerca de los resultados de las acciones es que siempre puedes escribir los tuyos con bastante facilidad si quieres hacer algo ligeramente diferente. Así es como puede hacerlo en su caso (suponiendo que desee el mensaje de error en texto / plano; si desea JSON, haría algo ligeramente diferente con el contenido):

public class NotFoundTextPlainActionResult : IHttpActionResult
{
    public NotFoundTextPlainActionResult(string message, HttpRequestMessage request)
    {
        if (message == null)
        {
            throw new ArgumentNullException("message");
        }

        if (request == null)
        {
            throw new ArgumentNullException("request");
        }

        Message = message;
        Request = request;
    }

    public string Message { get; private set; }

    public HttpRequestMessage Request { get; private set; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute());
    }

    public HttpResponseMessage Execute()
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.NotFound);
        response.Content = new StringContent(Message); // Put the message in the response body (text/plain content).
        response.RequestMessage = Request;
        return response;
    }
}

public static class ApiControllerExtensions
{
    public static NotFoundTextPlainActionResult NotFound(this ApiController controller, string message)
    {
        return new NotFoundTextPlainActionResult(message, controller.Request);
    }
}

Luego, en su método de acción, puede hacer algo como esto:

public class TestController : ApiController
{
    public IHttpActionResult Get()
    {
        return this.NotFound("These are not the droids you're looking for.");
    }
}

Si usó una clase base de controlador personalizado (en lugar de heredar directamente de ApiController), también podría eliminar el "esto". part (que desafortunadamente se requiere cuando se llama a un método de extensión):

public class CustomApiController : ApiController
{
    protected NotFoundTextPlainActionResult NotFound(string message)
    {
        return new NotFoundTextPlainActionResult(message, Request);
    }
}

public class TestController : CustomApiController
{
    public IHttpActionResult Get()
    {
        return NotFound("These are not the droids you're looking for.");
    }
}
dmatson
fuente
1
Escribí una implementación exactamente similar de 'IHttpActionResult', pero no específica para el resultado de 'NotFound'. Esto probablemente funcionará para todos los 'HttpStatusCodes'. Mi código CustomActionResult se parece a esto Y la acción 'Get ()' de mi controlador se ve así: 'public IHttpActionResult Get () {return CustomNotFoundResult ("Meessage to Return."); } 'Además, registré un error en CodePlex por considerar esto en la versión futura.
Ajay Jadhav
Yo uso ODataControllers y tuve que usar this.NotFound ("bla");
tarde
1
Muy buena publicación, pero me gustaría recomendar contra la sugerencia de herencia. Mi equipo decidió hacer exactamente eso hace mucho tiempo, e hinchó mucho las clases al hacerlo. Recientemente lo refactoricé todo en métodos de extensión y me alejé de la cadena de herencia. Recomendaría seriamente a las personas que consideren cuidadosamente cuándo deberían usar una herencia como esta. Por lo general, la composición es mucho mejor, porque está mucho más desacoplada.
julealgon
6
Esta funcionalidad debería haber estado lista para usar. La inclusión de un parámetro opcional "ResponseBody" no debería afectar las pruebas unitarias.
Theodore Zographos
230

Aquí hay una sola línea para devolver un IHttpActionResult NotFound con un mensaje simple:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");
Anthony F
fuente
24
La gente debería votar a favor de esta respuesta. ¡Es agradable y fácil!
Jess
2
Tenga en cuenta que esta solución no establece el estado del encabezado HTTP en "404 No encontrado".
Kasper Halvas Jensen
4
@KasperHalvasJensen El código de estado http del servidor es 404, ¿necesitas algo más?
Anthony F
4
@AnthonyF Tienes razón. Estaba usando Controller.Content (...). Debí haber usado el ApiController.Content (...) - Mi mal.
Kasper Halvas Jensen
Gracias amigo, esto era exactamente lo que estaba buscando
Kaptein Babbalas
28

Podría usar ResponseMessageResultsi lo desea:

var myCustomMessage = "your custom message which would be sent as a content-negotiated response"; 
return ResponseMessage(
    Request.CreateResponse(
        HttpStatusCode.NotFound, 
        myCustomMessage
    )
);

sí, si necesita versiones mucho más cortas, supongo que debe implementar el resultado de su acción personalizada.

Kiran Challa
fuente
Seguí este método porque me pareció genial. Acabo de definir el mensaje personalizado en otro lugar y sangré el código de retorno.
ozzy432836
Me gusta esto más que Content porque en realidad devuelve un objeto que puedo analizar con una propiedad de Message al igual que el método BadRequest estándar.
user1568891
7

Puede usar la propiedad ReasonPhrase de la clase HttpResponseMessage

catch (Exception exception)
{
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)
  {
    ReasonPhrase = exception.Message
  });
}
Dmytro Rudenko
fuente
Gracias. Bueno ... esto debería funcionar, pero luego tendré que construir la HttpResponseException por mi cuenta en cada acción. Para mantener el código menos, estaba pensando si podía usar cualquier característica de WebApi 2 (como los métodos NotFount () , Ok ( ) ya preparados) y pasarle el mensaje ReasonPhrase.
Ajay Jadhav
Puede crear su propio método de extensión NotFound (excepción de excepción), que arrojará la HttpResponseException correcta
Dmytro Rudenko
@DmytroRudenko: se introdujeron los resultados de la acción para mejorar la capacidad de prueba. Al lanzar HttpResponseException aquí, estaría comprometiendo eso. También aquí no tenemos ninguna excepción, pero el OP está buscando devolver un mensaje.
Kiran Challa
Ok, si no desea usar NUint para las pruebas, puede escribir su propia implementación de NotFoundResult y reescribir su ExecuteAsync para devolver los datos de su mensaje. Y devuelva una instancia de esta clase como resultado de la invocación de su acción.
Dmytro Rudenko
1
Tenga en cuenta que ahora puede pasar el código de estado directamente, por ejemplo, HttpResponseException (HttpStatusCode.NotFound)
Mark Sowul
3

Puede crear un resultado de contenido negociado personalizado como sugirió d3m3t3er. Sin embargo, heredaría de. Además, si solo lo necesita para devolver NotFound, no necesita inicializar el estado http desde el constructor.

public class NotFoundNegotiatedContentResult<T> : NegotiatedContentResult<T>
{
    public NotFoundNegotiatedContentResult(T content, ApiController controller)
        : base(HttpStatusCode.NotFound, content, controller)
    {
    }

    public override Task<HttpResponseMessage> ExecuteAsync(
        CancellationToken cancellationToken)
    {
        return base.ExecuteAsync(cancellationToken).ContinueWith(
            task => task.Result, cancellationToken);
    }
}
Andrei S
fuente
2

Lo resolví simplemente derivando OkNegotiatedContentResulty anulando el código HTTP en el mensaje de respuesta resultante. Esta clase le permite devolver el cuerpo del contenido con cualquier código de respuesta HTTP.

public class CustomNegotiatedContentResult<T> : OkNegotiatedContentResult<T>
{
    public HttpStatusCode HttpStatusCode;

    public CustomNegotiatedContentResult(
        HttpStatusCode httpStatusCode, T content, ApiController controller)
        : base(content, controller)
    {
        HttpStatusCode = httpStatusCode;
    }

    public override Task<HttpResponseMessage> ExecuteAsync(
        CancellationToken cancellationToken)
    {
        return base.ExecuteAsync(cancellationToken).ContinueWith(
            task => { 
                // override OK HTTP status code with our own
                task.Result.StatusCode = HttpStatusCode;
                return task.Result;
            },
            cancellationToken);
    }
}
demeter
fuente
1

Si hereda de la base NegotitatedContentResult<T>, como se mencionó, y no necesita transformar su content(por ejemplo, solo desea devolver una cadena), entonces no necesita anular el ExecuteAsyncmétodo.

Todo lo que necesita hacer es proporcionar una definición de tipo adecuada y un constructor que le indique a la base qué código de estado HTTP debe devolver. Todo lo demás simplemente funciona.

Aquí hay ejemplos para ambos NotFoundy InternalServerError:

public class NotFoundNegotiatedContentResult : NegotiatedContentResult<string>
{
    public NotFoundNegotiatedContentResult(string content, ApiController controller)
        : base(HttpStatusCode.NotFound, content, controller) { }
}

public class InternalServerErrorNegotiatedContentResult : NegotiatedContentResult<string>
{
    public InternalServerErrorNegotiatedContentResult(string content, ApiController controller)
        : base(HttpStatusCode.InternalServerError, content, controller) { }
}

Y luego puede crear los métodos de extensión correspondientes para ApiController(o hacerlo en una clase base si tiene una):

public static NotFoundNegotiatedContentResult NotFound(this ApiController controller, string message)
{
    return new NotFoundNegotiatedContentResult(message, controller);
}

public static InternalServerErrorNegotiatedContentResult InternalServerError(this ApiController controller, string message)
{
    return new InternalServerErrorNegotiatedContentResult(message, controller);
}

Y luego funcionan como los métodos integrados. Puede llamar al existente NotFound()o puede llamar a su nuevo personalizado NotFound(myErrorMessage).

Y, por supuesto, puede deshacerse de los tipos de cadenas "codificados" en las definiciones de tipos personalizados y dejarlos genéricos si lo desea, pero luego puede que tenga que preocuparse por las ExecuteAsynccosas, dependiendo de lo que <T>realmente sea.

Puede revisar el código fuente para NegotiatedContentResult<T>ver todo lo que hace. No hay mucho que hacer.

sliderhouserules
fuente
1

Necesitaba crear una IHttpActionResultinstancia en el cuerpo de una IExceptionHandlerclase para establecer la ExceptionHandlerContext.Resultpropiedad. Sin embargo, también quería establecer un archivo personalizado ReasonPhrase.

Descubrí que a ResponseMessageResultpodría envolver a HttpResponseMessage(lo que permite que ReasonPhrase se configure fácilmente).

Por ejemplo:

public class MyExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        var ex = context.Exception as IRecordNotFoundException;
        if (ex != null)
        {
            context.Result = new ResponseMessageResult(new HttpResponseMessage(HttpStatusCode.NotFound) { ReasonPhrase = $"{ex.EntityName} not found" });
        }
    }
}
Jono Job
fuente
0

Sé que PO preguntó con un mensaje de texto, pero otra opción para devolver un 404 es hacer que el método devuelva un IHttpActionResult y use la función StatusCode

    public async Task<IHttpActionResult> Get([FromUri]string id)
    {
       var item = await _service.GetItem(id);
       if(item == null)
       {
           StatusCode(HttpStatusCode.NotFound);
       }
       return Ok(item);
    }
Maykool Jiménez
fuente
0

A las respuestas aquí les falta un pequeño problema de la historia del desarrollador. La ApiControllerclase todavía expone un NotFound()método que los desarrolladores pueden usar. Esto provocaría que alguna respuesta 404 contenga un cuerpo de resultado no controlado.

Presento aquí algunas partes del código " mejor método ApiController NotFound " que proporcionará un método menos propenso a errores que no requiere que los desarrolladores conozcan "la mejor manera de enviar un 404".

  • crear una clase heredada de laApiController llamadaApiController
    • Utilizo esta técnica para evitar que los desarrolladores usen la clase original
  • anula su NotFoundmétodo para permitir que los desarrolladores usen la primera API disponible
  • si desea desalentar esto, márquelo como [Obsolete("Use overload instead")]
  • añade un extra protected NotFoundResult NotFound(string message)que quieras animar
  • problema: el resultado no admite responder con un cuerpo. solución: heredar y usar NegotiatedContentResult. ver adjunta mejor la clase NotFoundResult .
SandRock
fuente