Jak zwrócić NotFound () IHttpActionResult z komunikatem o błędzie lub wyjątkiem?

98

Zwracam NotFound IHttpActionResult, gdy coś nie zostało znalezione w mojej akcji WebApi GET. Wraz z tą odpowiedzią chcę wysłać niestandardową wiadomość i / lub wiadomość o wyjątku (jeśli istnieje). Obecny ApiControllerjest NotFound()metoda nie zapewnia przeciążenie przekazać wiadomość.

Czy jest na to sposób? czy będę musiał napisać własny zwyczaj IHttpActionResult?

Ajay Jadhav
źródło
Czy chcesz zwrócić tę samą wiadomość dla wszystkich wyników Nie znaleziono?
Nikolai Samteladze
@NikolaiSamteladze Nie, w zależności od sytuacji może to być inna wiadomość.
Ajay Jadhav

Odpowiedzi:

84

Jeśli chcesz dostosować kształt wiadomości z odpowiedzią, musisz napisać własny wynik akcji.

Chcieliśmy udostępnić najczęstsze kształty wiadomości odpowiedzi od razu po wyjęciu z pudełka dla rzeczy takich jak proste puste błędy 404, ale chcieliśmy również, aby te wyniki były jak najprostsze; Jedną z głównych zalet używania wyników akcji jest to, że znacznie ułatwia to test jednostkowy metody akcji. Im więcej właściwości przypisujemy wynikom akcji, tym więcej rzeczy musi wziąć pod uwagę test jednostkowy, aby upewnić się, że metoda akcji działa zgodnie z oczekiwaniami.

Często chcę również mieć możliwość dostarczania niestandardowej wiadomości, więc nie krępuj się, aby zarejestrować błąd, abyśmy mogli rozważyć wsparcie tego działania w przyszłej wersji: https://aspnetwebstack.codeplex.com/workitem/list/advanced

Jedną fajną rzeczą w wynikach działania jest jednak to, że zawsze możesz dość łatwo napisać własne, jeśli chcesz zrobić coś nieco innego. Oto, jak możesz to zrobić w swoim przypadku (zakładając, że chcesz, aby komunikat o błędzie był tekstowy / zwykły; jeśli chcesz JSON, zrobiłbyś coś nieco innego z zawartością):

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

Następnie w swojej metodzie akcji możesz po prostu zrobić coś takiego:

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

Jeśli użyłeś niestandardowej klasy bazowej kontrolera (zamiast bezpośrednio dziedziczyć po ApiController), możesz również wyeliminować „this”. część (która jest niestety wymagana przy wywołaniu metody rozszerzenia):

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
źródło
1
Napisałem dokładnie podobną implementację „IHttpActionResult”, ale nie specyficzną dla wyniku „NotFound”. To prawdopodobnie zadziała dla wszystkich „HttpStatusCodes”. Mój kod CustomActionResult wygląda coś jak ten i mój kontroler za 'get ()' wygląd akcji tak: „publiczne IHttpActionResult get () {return CustomNotFoundResult ( " Meessage powrotu"); } 'Ponadto, zarejestrowałem błąd w CodePlex, aby rozważyć to w przyszłej wersji.
Ajay Jadhav
Używam ODataControllers i musiałem użyć this.NotFound ("bla");
Jerther,
1
Bardzo fajny post, ale chciałbym tylko odrzucić wskazówkę dotyczącą dziedziczenia. Mój zespół zdecydował się to zrobić już dawno temu i robiąc to, bardzo rozdęł zajęcia. Niedawno przełożyłem to wszystko na metody rozszerzające i odszedłem od łańcucha dziedziczenia. Poważnie zalecałbym ludziom dokładne rozważenie, kiedy powinni używać dziedziczenia w ten sposób. Zwykle kompozycja jest dużo lepsza, ponieważ jest dużo bardziej odsprzęgnięta.
julealgon
6
Ta funkcja powinna być gotowa do użycia. Dołączenie opcjonalnego parametru „ResponseBody” nie powinno wpływać na testy jednostkowe.
Theodore Zographos
230

Oto jednowierszowy zwracający IHttpActionResult NotFound z prostą wiadomością:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");
Anthony F.
źródło
24
Ludzie powinni zagłosować na tę odpowiedź. To łatwe i przyjemne!
Jess
2
Należy pamiętać, że to rozwiązanie nie ustawia stanu nagłówka HTTP na „404 Not Found”.
Kasper Halvas Jensen
4
@KasperHalvasJensen Kod statusu http z serwera to 404, czy potrzebujesz czegoś więcej?
Anthony F
4
@AnthonyF Masz rację. Korzystałem z kontrolera. Treść (...). Powinienem użyć ApiController. Treść (...) - Moja wina.
Kasper Halvas Jensen
Dzięki kolego, właśnie tego szukałem
Kaptein Babbalas
28

Możesz użyć, ResponseMessageResultjeśli chcesz:

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

tak, jeśli potrzebujesz znacznie krótszych wersji, myślę, że musisz zaimplementować wynik działania niestandardowego.

Kiran Challa
źródło
Poszedłem z tą metodą, ponieważ wydawała się zgrabna. Właśnie zdefiniowałem niestandardową wiadomość w innym miejscu i wciąłem kod powrotu.
ozzy432836
Podoba mi się to bardziej niż Content, ponieważ w rzeczywistości zwraca obiekt, który mogę przeanalizować za pomocą właściwości Message, tak jak standardowa metoda BadRequest.
user1568891
7

Możesz użyć właściwości ReasonPhrase klasy HttpResponseMessage

catch (Exception exception)
{
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)
  {
    ReasonPhrase = exception.Message
  });
}
Dmytro Rudenko
źródło
Dzięki. Cóż ... to powinno zadziałać, ale wtedy będę musiał samodzielnie zbudować HttpResponseException w każdej akcji. Aby ograniczyć kod, zastanawiałem się, czy mógłbym użyć jakichkolwiek funkcji WebApi 2 (tak jak gotowe metody NotFount () , Ok () ) i przekazać do niego wiadomość ReasonPhrase.
Ajay Jadhav
Możesz stworzyć swoją własną metodę rozszerzenia NOTFOUND (wyjątek Exception), która rzuci poprawnego HttpResponseException
Dmytro Rudenko
@DmytroRudenko: wyniki akcji zostały wprowadzone w celu poprawy testowalności. Rzucając tutaj HttpResponseException, naruszysz to. Również tutaj nie mamy żadnego wyjątku, ale OP chce odesłać wiadomość.
Kiran Challa
Ok, jeśli nie chcesz używać NUint do testowania, możesz napisać własną implementację NotFoundResult i przepisać jej ExecuteAsync w celu zwrócenia danych wiadomości. I zwróć wystąpienie tej klasy w wyniku wywołania akcji.
Dmytro Rudenko
1
Zauważ, że teraz możesz przekazać kod statusu bezpośrednio, np. HttpResponseException (HttpStatusCode.NotFound)
Mark Sowul
3

Możesz utworzyć niestandardowy wynik negocjowanej zawartości zgodnie z sugestią d3m3t3er. Jednak odziedziczyłbym po. Ponadto, jeśli potrzebujesz go tylko do zwracania NotFound, nie musisz inicjować statusu http z konstruktora.

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
źródło
2

Rozwiązałem to, po prostu wyprowadzając OkNegotiatedContentResulti zastępując kod HTTP w wynikowym komunikacie odpowiedzi. Ta klasa umożliwia zwrócenie treści z dowolnym kodem odpowiedzi 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
źródło
1

Jeśli dziedziczysz z bazy NegotitatedContentResult<T>, jak wspomniano, i nie musisz przekształcać swojego content(np. Chcesz tylko zwrócić ciąg znaków), nie musisz nadpisywać ExecuteAsyncmetody.

Wszystko, co musisz zrobić, to podać odpowiednią definicję typu i konstruktora, który powie bazie, który kod stanu HTTP ma zwrócić. Wszystko inne po prostu działa.

Oto przykłady dla obu NotFoundi 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) { }
}

Następnie możesz utworzyć odpowiednie metody rozszerzające dla ApiController(lub zrobić to w klasie bazowej, jeśli taką masz):

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

A potem działają tak samo, jak metody wbudowane. Możesz zadzwonić do istniejącego NotFound()lub możesz zadzwonić do nowego niestandardowego NotFound(myErrorMessage).

I oczywiście, można pozbyć się „zakodowane” typów łańcuchowych w definicjach Typ niestandardowy i pozostawić go rodzajowe, jeśli chcesz, ale wtedy może trzeba się martwić o ExecuteAsyncrzeczy, w zależności od Państwa<T> rzeczywistości.

Można patrzeć na kod źródłowy dla NegotiatedContentResult<T>zobaczyć wszystko, co robi. Nie ma w tym wiele.

sliderhouserules
źródło
1

Musiałem utworzyć IHttpActionResultinstancję w treści IExceptionHandlerklasy, aby ustawić ExceptionHandlerContext.Resultwłaściwość. Jednak chciałem też ustanowić zwyczajReasonPhrase .

Odkryłem, że ResponseMessageResultmoże owinąć plikHttpResponseMessage (co umożliwia łatwe ustawienie ReasonPhrase).

Na przykład:

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
źródło
0

Wiem, PO zapytał o tekst wiadomości, ale inną opcją, aby po prostu zwrócić 404, jest zwrócenie przez metodę IHttpActionResult i użycie funkcji 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
źródło
0

W odpowiedziach brakuje małego problemu z historią programistów. ApiControllerKlasa wciąż odsłaniając NotFound()metody, które programiści mogą wykorzystać. To spowodowałoby, że odpowiedź 404 zawierałaby niekontrolowaną treść wyniku.

Przedstawiam tutaj kilka fragmentów kodu „ lepszej metody ApiController NotFound ”, która zapewni mniej podatną na błędy metodę, która nie wymaga od programistów znajomości „lepszego sposobu wysłania błędu 404”.

  • utwórz klasę dziedziczącą poApiController nazwieApiController
    • Używam tej techniki, aby uniemożliwić programistom używanie oryginalnej klasy
  • zastępuje jego NotFoundmetodę, aby umożliwić programistom użycie pierwszego dostępnego interfejsu API
  • jeśli chcesz to zniechęcić, oznacz to jako [Obsolete("Use overload instead")]
  • dodaj dodatkowy protected NotFoundResult NotFound(string message) , które chcesz zachęcić
  • problem: wynik nie wspiera odpowiedzi za pomocą ciała. rozwiązanie: dziedzicz i używaj NegotiatedContentResult. zobacz załączoną lepszą klasę NotFoundResult .
SandRock
źródło