Come restituisco NotFound () IHttpActionResult con un messaggio di errore o un'eccezione?


98

Sto restituendo un NotFound IHttpActionResult, quando qualcosa non viene trovato nella mia azione WebApi GET. Insieme a questa risposta, desidero inviare un messaggio personalizzato e / o il messaggio di eccezione (se presente). L'attuale ApiController's NotFound()metodo non fornisce un sovraccarico per passare un messaggio.

C'è un modo per farlo? o dovrò scrivere la mia abitudine IHttpActionResult?


Vuoi restituire lo stesso messaggio per tutti i risultati Non trovato?
Nikolai Samteladze

@NikolaiSamteladze No, potrebbe essere un messaggio diverso a seconda della situazione.
Ajay Jadhav

Risposte:


84

Dovresti scrivere il risultato dell'azione se desideri personalizzare la forma del messaggio di risposta.

Volevamo fornire le forme dei messaggi di risposta più comuni fuori dagli schemi per cose come semplici 404 vuoti, ma volevamo anche mantenere questi risultati il ​​più semplici possibile; uno dei principali vantaggi dell'utilizzo dei risultati dell'azione è che rende il metodo di azione molto più facile da testare. Più proprietà mettiamo sui risultati dell'azione, più cose deve considerare il tuo unit test per assicurarti che il metodo di azione stia facendo quello che ti aspetteresti.

Spesso desidero la possibilità di fornire anche un messaggio personalizzato, quindi sentiti libero di registrare un bug per farci considerare il supporto di tale azione in una versione futura: https://aspnetwebstack.codeplex.com/workitem/list/advanced

Una cosa bella dei risultati delle azioni, però, è che puoi sempre scrivere la tua abbastanza facilmente se vuoi fare qualcosa di leggermente diverso. Ecco come potresti farlo nel tuo caso (supponendo che tu voglia il messaggio di errore in text / plain; se vuoi JSON, faresti qualcosa di leggermente diverso con il contenuto):

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

Quindi, nel tuo metodo di azione, puoi semplicemente fare qualcosa del genere:

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

Se hai utilizzato una classe di base del controller personalizzato (invece di ereditare direttamente da ApiController), potresti anche eliminare "questo". parte (che è purtroppo richiesta quando si chiama un metodo di estensione):

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

1
Ho scritto un'implementazione esattamente simile di "IHttpActionResult", ma non specifica per il risultato "NotFound". Questo probabilmente funzionerà per tutti gli "HttpStatusCodes". Il mio codice CustomActionResult è simile a questo E l'azione 'Get ()' del mio controllore è simile a questa: 'public IHttpActionResult Get () {return CustomNotFoundResult ("Meessage to Return."); } 'Inoltre, ho registrato un bug su CodePlex per aver considerato questo nella versione futura.
Ajay Jadhav

Uso ODataControllers e ho dovuto usare this.NotFound ("blah");
Jerther

1
Post molto carino, ma vorrei solo sconsigliare la mancia sull'eredità. La mia squadra ha deciso di fare esattamente questo molto tempo fa e ha gonfiato molto le classi facendolo. Di recente ho refactoring tutto in metodi di estensione e mi sono allontanato dalla catena di ereditarietà. Raccomanderei seriamente alle persone di considerare attentamente quando dovrebbero usare l'ereditarietà come questa. Di solito, la composizione è molto migliore, perché è molto più disaccoppiata.
julealgon

6
Questa funzionalità avrebbe dovuto essere pronta all'uso. L'inclusione di un parametro "ResponseBody" facoltativo non dovrebbe influire sugli unit test.
Theodore Zographos

230

Ecco una battuta per restituire un IHttpActionResult NotFound con un semplice messaggio:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");

24
La gente dovrebbe votare a favore di questa risposta. È bello e facile!
Jess

2
Tieni presente che questa soluzione non imposta lo stato dell'intestazione HTTP su "404 Not Found".
Kasper Halvas Jensen

4
@KasperHalvasJensen Il codice di stato http del server è 404, hai bisogno di qualcosa di più?
Anthony F

4
@AnthonyF Hai ragione. Stavo usando il Controller.Content (...). Shoud ha usato l'ApiController.Content (...) - Colpa mia.
Kasper Halvas Jensen

Grazie amico, questo era esattamente quello che stavo cercando
Kaptein Babbalas

28

Potresti usare ResponseMessageResultse ti piace:

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

sì, se hai bisogno di versioni molto più brevi, immagino che tu debba implementare il risultato dell'azione personalizzata.


Sono andato con questo metodo perché sembrava pulito. Ho appena definito il messaggio personalizzato altrove e il codice di ritorno rientrato.
ozzy432836

Mi piace meglio di Content perché restituisce effettivamente un oggetto che posso analizzare con una proprietà Message proprio come il metodo BadRequest standard.
user1568891

7

È possibile utilizzare la proprietà ReasonPhrase della classe HttpResponseMessage

catch (Exception exception)
{
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)
  {
    ReasonPhrase = exception.Message
  });
}

Grazie. Bene ... questo dovrebbe funzionare, ma poi dovrò costruire HttpResponseException da solo in ogni azione. Per mantenere il codice meno, stavo pensando se potevo usare qualsiasi funzionalità di WebApi 2 (proprio come i metodi NotFount () , Ok () già pronti ) e passargli il messaggio ReasonPhrase.
Ajay Jadhav

Puoi creare il tuo metodo di estensione NotFound (eccezione di eccezione), che genererà HttpResponseException corretta
Dmytro Rudenko

@DmytroRudenko: sono stati introdotti i risultati dell'azione per migliorare la testabilità. Lanciando HttpResponseException qui lo comprometteresti. Anche qui non abbiamo eccezioni, ma l'OP sta cercando di rimandare un messaggio.
Kiran Challa

Ok, se non vuoi usare NUint per i test, puoi scrivere la tua implementazione di NotFoundResult e riscrivere il suo ExecuteAsync per restituire i dati del tuo messaggio. E restituisce l'istanza di questa classe come risultato della chiamata all'azione.
Dmytro Rudenko

1
Tieni presente che ora puoi passare direttamente il codice di stato, ad esempio HttpResponseException (HttpStatusCode.NotFound)
Mark Sowul,

3

È possibile creare un risultato di contenuto negoziato personalizzato come suggerito da d3m3t3er. Tuttavia avrei ereditato da. Inoltre, se ne hai bisogno solo per restituire NotFound, non è necessario inizializzare lo stato http dal costruttore.

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

2

L'ho risolto semplicemente derivando OkNegotiatedContentResulte sovrascrivendo il codice HTTP nel messaggio di risposta risultante. Questa classe consente di restituire il corpo del contenuto con qualsiasi codice di risposta 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);
    }
}

1

Se erediti dalla base NegotitatedContentResult<T>, come accennato, e non hai bisogno di trasformare il tuo content(ad esempio, vuoi solo restituire una stringa), allora non hai bisogno di sovrascrivere il ExecuteAsyncmetodo.

Tutto quello che devi fare è fornire una definizione di tipo appropriata e un costruttore che indichi alla base quale codice di stato HTTP restituire. Tutto il resto funziona.

Ecco alcuni esempi per entrambi 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 poi puoi creare metodi di estensione corrispondenti per ApiController(o farlo in una classe base se ne hai uno):

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 poi funzionano proprio come i metodi integrati. Puoi chiamare l'esistente NotFound()o puoi chiamare la tua nuova abitudine NotFound(myErrorMessage).

E, naturalmente, puoi sbarazzarti dei tipi di stringa "hard-coded" nelle definizioni dei tipi personalizzati e lasciarli generici se vuoi, ma poi potresti doverti preoccupare delle ExecuteAsynccose, a seconda di cosa sei <T>effettivamente.

Puoi controllare il codice sorgente per NegotiatedContentResult<T>vedere tutto ciò che fa. Non c'è molto da fare.


1

Avevo bisogno di creare IHttpActionResultun'istanza nel corpo di una IExceptionHandlerclasse, al fine di impostare la ExceptionHandlerContext.Resultproprietà. Tuttavia volevo anche impostare un'abitudine ReasonPhrase.

Ho scoperto che a ResponseMessageResultpotrebbe avvolgere a HttpResponseMessage(che consente di impostare facilmente ReasonPhrase).

Per esempio:

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

0

So che il PO è stato chiesto con un messaggio di testo, ma un'altra opzione per restituire solo un 404 è fare in modo che il metodo restituisca un IHttpActionResult e utilizzi la funzione StatusCode

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

0

Alle risposte qui manca un piccolo problema con la storia dello sviluppatore. La ApiControllerclasse sta ancora esponendo un NotFound()metodo che gli sviluppatori possono utilizzare. Ciò farebbe sì che una risposta 404 contenga un corpo dei risultati non controllato.

Presento qui alcune parti del codice " metodo ApiController NotFound migliore " che fornirà un metodo meno soggetto a errori che non richiede agli sviluppatori di conoscere "il modo migliore per inviare un 404".

  • creare una classe che eredita daApiController chiamataApiController
    • Uso questa tecnica per impedire agli sviluppatori di utilizzare la classe originale
  • sovrascrivi il suo NotFoundmetodo per consentire agli sviluppatori di utilizzare la prima API disponibile
  • se vuoi scoraggiarlo, contrassegnalo come [Obsolete("Use overload instead")]
  • aggiungi un extra protected NotFoundResult NotFound(string message)che vuoi incoraggiare
  • problema: il risultato non supporta la risposta con un corpo. soluzione: ereditare e utilizzare NegotiatedContentResult. vedere la migliore classe NotFoundResult allegata .
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.