Como retorno NotFound () IHttpActionResult com uma mensagem de erro ou exceção?

98

Estou retornando um NotFound IHttpActionResult, quando algo não é encontrado na minha ação GET do WebApi. Junto com esta resposta, desejo enviar uma mensagem personalizada e / ou a mensagem de exceção (se houver). O atual ApiController's NotFound()método não fornece uma sobrecarga para passar uma mensagem.

Existe alguma maneira de fazer isso? ou terei que escrever meu próprio costume IHttpActionResult?

Ajay Jadhav
fonte
Deseja retornar a mesma mensagem para todos os resultados Não encontrados?
Nikolai Samteladze
@NikolaiSamteladze Não, pode ser uma mensagem diferente dependendo da situação.
Ajay Jadhav

Respostas:

84

Você precisaria escrever seu próprio resultado de ação se quiser personalizar o formato da mensagem de resposta.

Queríamos fornecer os formatos de mensagem de resposta mais comuns prontos para uso, como 404s vazios simples, mas também queríamos manter esses resultados o mais simples possível; uma das principais vantagens de usar resultados de ação é que torna o seu método de ação muito mais fácil para o teste de unidade. Quanto mais propriedades colocamos nos resultados da ação, mais coisas seu teste de unidade precisa considerar para ter certeza de que o método de ação está fazendo o que você espera.

Freqüentemente, desejo também a capacidade de fornecer uma mensagem personalizada, então sinta-se à vontade para registrar um bug para que possamos considerar apoiar esse resultado de ação em uma versão futura: https://aspnetwebstack.codeplex.com/workitem/list/advanced

Uma coisa boa sobre os resultados da ação, porém, é que você sempre pode escrever seus próprios facilmente se quiser fazer algo ligeiramente diferente. Veja como você pode fazer no seu caso (supondo que você queira a mensagem de erro em texto / simples; se quiser JSON, faria algo um pouco diferente com o conteúdo):

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);
    }
}

Então, em seu método de ação, você pode simplesmente fazer algo assim:

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

Se você usou uma classe base do controlador personalizado (em vez de herdar diretamente do ApiController), também pode eliminar o "this". parte (que infelizmente é necessária ao chamar um método de extensão):

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
fonte
1
Escrevi uma implementação exatamente semelhante de 'IHttpActionResult', mas não específica para o resultado 'NotFound'. Isso provavelmente funcionará para todos os 'HttpStatusCodes'. Meu código CustomActionResult se parece com isto E a ação 'Get ()' do meu controlador se parece com isto: 'public IHttpActionResult Get () {return CustomNotFoundResult ("Meessage to Return."); } 'Além disso, registrei um bug no CodePlex por considerar isso em uma versão futura.
Ajay Jadhav
Eu uso ODataControllers e tive que usar this.NotFound ("blah");
Jerther
1
Postagem muito legal, mas eu só gostaria de recomendar contra a dica de herança. Minha equipe decidiu fazer exatamente isso há muito tempo, e isso inchou muito as aulas. Recentemente, refatorei tudo isso em métodos de extensão e me afastei da cadeia de herança. Eu recomendaria seriamente às pessoas que considerassem cuidadosamente quando deveriam usar herança como esta. Normalmente, a composição é muito melhor, porque é muito mais desacoplada.
julealgon
6
Essa funcionalidade deveria estar pronta para uso. Incluir um parâmetro opcional "ResponseBody" não deve afetar os testes de unidade.
Theodore Zographos
230

Aqui está uma linha para retornar um IHttpActionResult NotFound com uma mensagem simples:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");
Anthony F
fonte
24
As pessoas deveriam votar esta resposta. É bom e fácil!
Jess de
2
Esteja ciente de que esta solução não define o status do cabeçalho HTTP como "404 Not Found".
Kasper Halvas Jensen
4
@KasperHalvasJensen O código de status http do servidor é 404, você precisa de algo mais?
Anthony F
4
@AnthonyF Você está certo. Eu estava usando o Controller.Content (...). Shoud tem usado o ApiController.Content (...) - Que pena.
Kasper Halvas Jensen
Obrigado amigo, era exatamente isso que eu procurava
Kaptein Babbalas,
28

Você pode usar ResponseMessageResultse quiser:

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

sim, se você precisa de versões mais curtas, então acho que você precisa implementar seu resultado de ação personalizada.

Kiran Challa
fonte
Eu escolhi este método porque parecia legal. Acabei de definir a mensagem personalizada em outro lugar e recuar o código de retorno.
ozzy432836
Gosto mais disso do que de Content porque, na verdade, ele retorna um objeto que posso analisar com uma propriedade Message exatamente como o método BadRequest padrão.
user1568891
7

Você pode usar a propriedade ReasonPhrase da classe HttpResponseMessage

catch (Exception exception)
{
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)
  {
    ReasonPhrase = exception.Message
  });
}
Dmytro Rudenko
fonte
Obrigado. Bem ... isso deve funcionar, mas então terei que construir a HttpResponseException sozinho em cada ação. Para manter o código menor, eu estava pensando se poderia usar qualquer recurso do WebApi 2 (assim como os métodos prontos NotFount () , Ok () ) e passar a mensagem ReasonPhrase para ele.
Ajay Jadhav
Você pode criar seu próprio método de extensão NotFound (exceção de exceção), que lançará HttpResponseException correta
Dmytro Rudenko
@DmytroRudenko: os resultados da ação foram introduzidos para melhorar a testabilidade. Ao lançar HttpResponseException aqui, você estaria comprometendo isso. Também aqui não temos nenhuma exceção, mas o OP está procurando enviar uma mensagem de volta.
Kiran Challa de
Ok, se você não quiser usar o NUint para testes, pode escrever sua própria implementação de NotFoundResult e reescrever seu ExecuteAsync para retornar os dados da mensagem. E retornar a instância desta classe como resultado de sua invocação de ação.
Dmytro Rudenko
1
Observe que agora você pode passar o código de status diretamente, por exemplo, HttpResponseException (HttpStatusCode.NotFound)
Mark Sowul
3

Você pode criar um resultado de conteúdo negociado personalizado conforme sugerido pelo d3m3t3er. No entanto, eu herdaria. Além disso, se você precisar apenas para retornar NotFound, não será necessário inicializar o status http do construtor.

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
fonte
2

Eu resolvi isso simplesmente derivando OkNegotiatedContentResulte substituindo o código HTTP na mensagem de resposta resultante. Esta classe permite que você retorne o corpo do conteúdo com qualquer código de resposta 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
fonte
1

Se você herda da base NegotitatedContentResult<T>, como mencionado, e não precisa transformar o seu content(por exemplo, você quer apenas retornar uma string), não precisa sobrescrever o ExecuteAsyncmétodo.

Tudo o que você precisa fazer é fornecer uma definição de tipo apropriada e um construtor que diga à base qual código de status HTTP retornar. Todo o resto simplesmente funciona.

Aqui estão alguns exemplos para NotFounde 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) { }
}

E então você pode criar métodos de extensão correspondentes para ApiController(ou fazer isso em uma classe base se você tiver uma):

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);
}

E então eles funcionam exatamente como os métodos integrados. Você pode chamar o existente NotFound()ou pode chamar o seu novo personalizado NotFound(myErrorMessage).

E, claro, você pode se livrar dos tipos de string "embutidos no código" nas definições de tipo personalizado e deixá-los genéricos se quiser, mas então você pode ter que se preocupar com as ExecuteAsynccoisas, dependendo do que você <T>realmente é.

Você pode examinar o código-fonte para NegotiatedContentResult<T>ver tudo o que ele faz. Não há muito a fazer.

sliderhouserules
fonte
1

Eu estava precisando criar uma IHttpActionResultinstância no corpo de uma IExceptionHandlerclasse, a fim de definir a ExceptionHandlerContext.Resultpropriedade. No entanto, também queria definir um costume ReasonPhrase.

Descobri que um ResponseMessageResultpoderia envolver um HttpResponseMessage(o que permite que ReasonPhrase seja definido facilmente).

Por exemplo:

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
fonte
0

Eu sei que PO perguntou com um texto de mensagem, mas outra opção para apenas retornar um 404 é fazer o método retornar um IHttpActionResult e usar a função 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 Jimenez
fonte
0

As respostas aqui estão faltando um pequeno problema de história do desenvolvedor. A ApiControllerclasse ainda está expondo um NotFound()método que os desenvolvedores podem usar. Isso faria com que alguma resposta 404 contivesse um corpo de resultado não controlado.

Apresento aqui algumas partes do código " melhor método ApiController NotFound " que fornecerá um método menos sujeito a erros que não exige que os desenvolvedores conheçam "a melhor maneira de enviar um 404".

  • criar uma classe herdada deApiController chamadaApiController
    • Eu uso essa técnica para evitar que os desenvolvedores usem a classe original
  • sobrescrever seu NotFoundmétodo para permitir que os desenvolvedores usem a primeira API disponível
  • se você quiser desencorajar isso, marque isso como [Obsolete("Use overload instead")]
  • adicione um extra protected NotFoundResult NotFound(string message)que você deseja encorajar
  • problema: o resultado não suporta responder com um corpo. solução: herdar e usar NegotiatedContentResult. veja em anexo a melhor classe NotFoundResult .
SandRock
fonte