Comment renvoyer NotFound () IHttpActionResult avec un message d'erreur ou une exception?

98

Je renvoie un NotFound IHttpActionResult, lorsque quelque chose n'est pas trouvé dans mon action WebApi GET. Parallèlement à cette réponse, je souhaite envoyer un message personnalisé et / ou le message d'exception (le cas échéant). Le courant ApiControllerde NotFound()méthode ne fournit pas une surcharge pour passer un message.

Y a-t-il une manière de faire ça? ou je devrai écrire ma propre coutume IHttpActionResult?

Ajay Jadhav
la source
Voulez-vous renvoyer le même message pour tous les résultats non trouvés?
Nikolai Samteladze
@NikolaiSamteladze Non, cela pourrait être un message différent selon la situation.
Ajay Jadhav

Réponses:

84

Vous devez écrire votre propre résultat d'action si vous souhaitez personnaliser la forme du message de réponse.

Nous voulions fournir les formes de message de réponse les plus courantes prêtes à l'emploi pour des choses comme de simples 404 vides, mais nous voulions également que ces résultats soient aussi simples que possible; l'un des principaux avantages de l'utilisation des résultats d'action est qu'elle rend votre méthode d'action beaucoup plus facile à tester unitaire. Plus nous attribuons de propriétés aux résultats d'action, plus votre test unitaire doit prendre en compte de choses pour s'assurer que la méthode d'action fait ce que vous attendez.

Je souhaite souvent pouvoir également fournir un message personnalisé, alors n'hésitez pas à enregistrer un bogue pour que nous envisagions de prendre en charge ce résultat d'action dans une prochaine version: https://aspnetwebstack.codeplex.com/workitem/list/advanced

Une bonne chose à propos des résultats d'action, cependant, est que vous pouvez toujours écrire les vôtres assez facilement si vous voulez faire quelque chose de légèrement différent. Voici comment vous pouvez le faire dans votre cas (en supposant que vous vouliez le message d'erreur en texte / brut; si vous voulez JSON, vous feriez quelque chose de légèrement différent avec le contenu):

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

Ensuite, dans votre méthode d'action, vous pouvez simplement faire quelque chose comme ceci:

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

Si vous avez utilisé une classe de base de contrôleur personnalisée (au lieu d'hériter directement d'ApiController), vous pouvez également éliminer le «ceci». part (qui est malheureusement nécessaire lors de l'appel d'une méthode d'extension):

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
la source
1
J'ai écrit une implémentation exactement similaire de 'IHttpActionResult', mais pas spécifique pour le résultat 'NotFound'. Cela fonctionnera probablement pour tous les «HttpStatusCodes». Mon code CustomActionResult ressemble à quelque chose comme ceci Et l'action 'Get ()' de mon contrôleur ressemble à ceci: 'public IHttpActionResult Get () {return CustomNotFoundResult ("Meessage to Return."); } 'De plus, j'ai signalé un bogue sur CodePlex pour avoir envisagé cela dans la prochaine version.
Ajay Jadhav
J'utilise ODataControllers et j'ai dû utiliser this.NotFound ("blah");
Jerther
1
Très bel article, mais je voudrais juste recommander contre le pourboire d'héritage. Mon équipe a décidé de faire exactement cela il y a longtemps, et cela a beaucoup gonflé les classes en le faisant. J'ai récemment tout remanié en méthodes d'extension et je me suis éloigné de la chaîne d'héritage. Je recommanderais sérieusement aux gens de réfléchir attentivement au moment où ils devraient utiliser l'héritage comme celui-ci. Habituellement, la composition est bien meilleure, car elle est beaucoup plus découplée.
julealgon
6
Cette fonctionnalité aurait dû être prête à l'emploi. L'inclusion d'un paramètre optionnel "ResponseBody" ne devrait pas affecter les tests unitaires.
Theodore Zographos
230

Voici une ligne pour renvoyer un IHttpActionResult NotFound avec un message simple:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");
Anthony F
la source
24
Les gens devraient voter pour cette réponse. C'est sympa et facile!
Jess
2
Sachez que cette solution ne définit pas l'état de l'en-tête HTTP sur «404 Not Found».
Kasper Halvas Jensen
4
@KasperHalvasJensen Le code d'état http du serveur est 404, avez-vous besoin de quelque chose de plus?
Anthony F
4
@AnthonyF Vous avez raison. J'utilisais le contrôleur.Contenu (...). Doit avoir utilisé le ApiController.Contenu (...) - Mon mauvais.
Kasper Halvas Jensen
Merci mon pote, c'était exactement ce que je cherchais
Kaptein Babbalas
28

Vous pouvez utiliser ResponseMessageResultsi vous le souhaitez:

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

oui, si vous avez besoin de versions beaucoup plus courtes, je suppose que vous devez implémenter votre résultat d'action personnalisé.

Kiran Challa
la source
Je suis allé avec cette méthode car elle me semblait soignée. Je viens de définir le message personnalisé ailleurs et le code de retour en retrait.
ozzy432836
J'aime mieux cela que Content car il renvoie en fait un objet que je peux analyser avec une propriété Message, tout comme la méthode BadRequest standard.
user1568891
7

Vous pouvez utiliser la propriété ReasonPhrase de la classe HttpResponseMessage

catch (Exception exception)
{
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)
  {
    ReasonPhrase = exception.Message
  });
}
Dmytro Rudenko
la source
Merci. Eh bien ... cela devrait fonctionner, mais je devrai alors créer mon propre HttpResponseException dans chaque action. Pour garder le code moins, je me demandais si je pouvais utiliser des fonctionnalités WebApi 2 (tout comme les méthodes NotFount () , Ok () prêtes à l'emploi ) et lui transmettre le message ReasonPhrase.
Ajay Jadhav
Vous pouvez créer votre propre méthode d'extension NotFound (exception exception), qui lancera une exception HttpResponseException correcte
Dmytro Rudenko
@DmytroRudenko: des résultats d'action ont été introduits pour améliorer la testabilité. En lançant HttpResponseException ici, vous compromettriez cela. Ici aussi, nous n'avons aucune exception, mais l'OP cherche à renvoyer un message.
Kiran Challa
Ok, si vous ne voulez pas utiliser NUint pour les tests, vous pouvez écrire votre propre implémentation de NotFoundResult et réécrire son ExecuteAsync pour renvoyer vos données de message. Et renvoyez l'instance de cette classe à la suite de votre appel d'action.
Dmytro Rudenko
1
Notez que maintenant vous pouvez passer le code de statut directement, par exemple HttpResponseException (HttpStatusCode.NotFound)
Mark Sowul
3

Vous pouvez créer un résultat de contenu négocié personnalisé comme le suggère d3m3t3er. Cependant j'hériterais de. De plus, si vous n'en avez besoin que pour renvoyer NotFound, vous n'avez pas besoin d'initialiser le statut http à partir du constructeur.

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
la source
2

Je l'ai résolu en dérivant simplement OkNegotiatedContentResultet en remplaçant le code HTTP dans le message de réponse résultant. Cette classe vous permet de renvoyer le corps du contenu avec n'importe quel code de réponse 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);
    }
}
Déméter
la source
1

Si vous héritez de la base NegotitatedContentResult<T>, comme mentionné, et que vous n'avez pas besoin de transformer votre content(par exemple, vous voulez simplement renvoyer une chaîne), vous n'avez pas besoin de remplacer la ExecuteAsyncméthode.

Tout ce que vous avez à faire est de fournir une définition de type appropriée et un constructeur qui indique à la base le code d'état HTTP à renvoyer. Tout le reste fonctionne.

Voici des exemples pour les deux NotFoundet 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) { }
}

Et puis vous pouvez créer des méthodes d'extension correspondantes pour ApiController(ou le faire dans une classe de base si vous en avez une):

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

Et puis, ils fonctionnent comme les méthodes intégrées. Vous pouvez soit appeler l'existant, NotFound()soit appeler votre nouvelle coutumeNotFound(myErrorMessage) .

Et bien sûr, vous pouvez vous débarrasser des types de chaînes "codés en dur" dans les définitions de type personnalisées et les laisser génériques si vous le souhaitez, mais vous devrez peut- être vous inquiéter de ExecuteAsyncce que vous<T> réellement.

Vous pouvez consulter le code source pour NegotiatedContentResult<T>voir tout ce qu'il fait. Il n'y a pas grand-chose à faire.

curseurs
la source
1

J'avais besoin de créer une IHttpActionResultinstance dans le corps d'une IExceptionHandlerclasse, afin de définir la ExceptionHandlerContext.Resultpropriété. Cependant, je voulais aussi définir uneReasonPhrase .

J'ai trouvé que a ResponseMessageResultpouvait envelopper un HttpResponseMessage(ce qui permet de définir facilement ReasonPhrase).

Par exemple:

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
la source
0

Iknow PO a demandé avec un texte de message, mais une autre option pour simplement renvoyer un 404 consiste à faire en sorte que la méthode retourne un IHttpActionResult et utilise la fonction 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
la source
0

Les réponses ici manquent un petit problème d'histoire de développeur. La ApiControllerclasse expose toujours unNotFound() méthode que les développeurs peuvent utiliser. Cela ferait en sorte qu'une réponse 404 contienne un corps de résultat incontrôlé.

Je présente ici quelques parties de code " meilleure méthode ApiController NotFound " qui fournira une méthode moins sujette aux erreurs qui ne demande pas aux développeurs de connaître "la meilleure façon d'envoyer un 404".

  • créer une classe héritant deApiController calledApiController
    • J'utilise cette technique pour empêcher les développeurs d'utiliser la classe d'origine
  • remplacer sa NotFoundméthode pour permettre aux développeurs d'utiliser la première API disponible
  • si vous voulez décourager cela, marquez-le comme [Obsolete("Use overload instead")]
  • ajouter un extra protected NotFoundResult NotFound(string message) que vous souhaitez encourager
  • problème: le résultat ne permet pas de répondre avec un corps. solution: hériter et utiliser NegotiatedContentResult. voir une meilleure classe NotFoundResult ci-jointe .
SandRock
la source