85 votes

Renvoi d'un 404 à partir d'un contrôleur API ASP.NET Core explicitement typé (pas IActionResult)

ASP.NET API de Base des contrôleurs renvoient généralement des types explicites (et de le faire par défaut si vous créez un nouveau projet), quelque chose comme:

[Route("api/[controller]")]
public class ThingsController : Controller
{
    // GET api/things
    [HttpGet]
    public async Task<IEnumerable<Thing>> GetAsync()
    {
        //...
    }

    // GET api/things/5
    [HttpGet("{id}")]
    public async Task<Thing> GetAsync(int id)
    {
        Thing thingFromDB = await GetThingFromDBAsync();
        if(thingFromDB == null)
            return null; // This returns HTTP 204

        // Process thingFromDB, blah blah blah
        return thing;
    }

    // POST api/things
    [HttpPost]
    public void Post([FromBody]Thing thing)
    {
        //..
    }

    //... and so on...
}

Le problème est qu' return null; - , elle retourne un HTTP 204: le succès, l'absence de contenu.

C'est alors considérée par un grand nombre de composants Javascript côté client que du succès, il y a un code comme:

const response = await fetch('.../api/things/5', {method: 'GET' ...});
if(response.ok)
    return await response.json(); // Error, no content!

Une recherche en ligne (comme cette question et cette réponse) points utiles return NotFound(); des méthodes d'extension pour le contrôleur, mais tous ces retour IActionResult, ce qui n'est pas compatible avec mon Task<Thing> type de retour. Que modèle de conception ressemble à ceci:

// GET api/things/5
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
    var thingFromDB = await GetThingFromDBAsync();
    if (thingFromDB == null)
        return NotFound();

    // Process thingFromDB, blah blah blah
    return Ok(thing);
}

Cela fonctionne, mais pour utiliser le type de retour d' GetAsync doit être changée en Task<IActionResult> - le typage explicite est perdu, et tous les types de retour sur le contrôleur avoir à changer (c'est à dire de ne pas utiliser explicitement saisie) ou il y aura un mélange où certaines actions de traiter avec des types explicites tandis que d'autres. En plus des tests unitaires maintenant besoin de faire des hypothèses sur la sérialisation et explicitement deserialise le contenu de l' IActionResult où avant ils avaient un type concret.

Il y a des tas de façons de contourner cela, mais il semble être une source de confusion méli-mélo qui pourrait facilement être conçus, donc la vraie question est: quelle est la bonne manière prévue par la ASP.NET de Base des designers?

Il semble que les options possibles sont:

  1. Ont une drôle de (salissant pour tester) le mélange de types explicites et IActionResult selon le type attendu.
  2. Oubliez les types explicites, ils ne sont pas vraiment pris en charge par Core, MVC, toujours utiliser IActionResult (dans ce cas, pourquoi sont-ils présents à tous?)
  3. Écrire une implémentation de l' HttpResponseException et l'utiliser comme ArgumentOutOfRangeException (voir cette réponse pour une mise en œuvre). Toutefois, cela ne requièrent l'utilisation d'exceptions pour les flux de programme, qui est généralement une mauvaise idée et également désapprouvées par la MVC du Noyau de l'équipe.
  4. Écrire une implémentation de l' HttpNoContentOutputFormatter qui renvoie 404 pour les requêtes GET.
  5. Autre chose qui me manque dans la façon dont Core, MVC est censé fonctionner?
  6. Ou pourquoi est - 204 est correcte et 404 mauvais pour l'échec d'une requête GET?

Toutes ces activités impliquent des compromis et refactoring de perdre quelque chose ou ajouter ce qui semble être la complexité inutile, en contradiction avec la conception de la MVC de Base. Les compromis qui est le bon et pourquoi?

87voto

Keith Points 46288

C'est abordée dans ASP.NET de Base 2.1 avec ActionResult<T>:

public ActionResult<Thing> Get(int id) {
    Thing thing = GetThingFromDB();

    if (thing == null)
        return NotFound();

    return thing;
}

Ou encore:

public ActionResult<Thing> Get(int id) =>
    GetThingFromDB() ?? NotFound();

Je vais mettre à jour cette réponse avec plus de détails une fois que j'ai mis en œuvre.

Réponse Originale À Cette Question

Dans ASP.NET l'API Web 5 il y avait un HttpResponseException (comme l'a souligné Hackerman), mais il a été retiré de Base et il n'y a pas de middleware pour la gérer.

Je pense que ce changement est dû .NET de Base - où ASP.NET essaie de faire tout ce qui sort de la boîte, ASP.NET de Base ne fait que ce que vous dire précisément ce qu'il (ce qui est une grande partie de pourquoi c'est tellement plus rapide et portable).

Je ne peux pas trouver une bibliothèque existante qui fait cela, donc je l'ai écrit moi-même. Nous avons d'abord besoin d'une exception personnalisée pour vérifier:

public class StatusCodeException : Exception
{
    public StatusCodeException(HttpStatusCode statusCode)
    {
        StatusCode = statusCode;
    }

    public HttpStatusCode StatusCode { get; set; }
}

Ensuite, nous avons besoin d'un RequestDelegate gestionnaire qui vérifie la présence de la nouvelle exception, et la convertit le statut de la réponse HTTP de code:

public class StatusCodeExceptionHandler
{
    private readonly RequestDelegate request;

    public StatusCodeExceptionHandler(RequestDelegate pipeline)
    {
        this.request = pipeline;
    }

    public Task Invoke(HttpContext context) => this.InvokeAsync(context); // Stops VS from nagging about async method without ...Async suffix.

    async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await this.request(context);
        }
        catch (StatusCodeException exception)
        {
            context.Response.StatusCode = (int)exception.StatusCode;
            context.Response.Headers.Clear();
        }
    }
}

Ensuite, nous enregistrons cette middleware dans notre Startup.Configure:

public class Startup
{
    ...

    public void Configure(IApplicationBuilder app)
    {
        ...
        app.UseMiddleware<StatusCodeExceptionHandler>();

Enfin, les actions peuvent jeter le code de statut HTTP exception, tout en renvoyant un type explicite qui peut facilement être l'objet de tests unitaires, sans la conversion de IActionResult:

public Thing Get(int id) {
    Thing thing = GetThingFromDB();

    if (thing == null)
        throw new StatusCodeException(HttpStatusCode.NotFound);

    return thing;
}

Cela permet de maintenir les types explicites pour les valeurs de retour et permet de faciliter la distinction entre le succès des résultats vides (return null;) et une erreur parce que quelque chose ne peut pas être trouvé (je pense à elle comme jeter un ArgumentOutOfRangeException).

Bien que ce soit une solution au problème, il n'est toujours pas vraiment répondre à ma question - les concepteurs de l'API Web créer un support pour les types explicites avec l'espoir qu'ils seront utilisés, a ajouté un traitement spécifique des return null; , de sorte qu'il serait de produire une 204 plutôt que de 200, et puis je n'ai pas de toute façon de traiter avec la 404? Il semble que beaucoup de travail pour ajouter quelque chose d'aussi simple.

15voto

David Pine Points 1964

Vous pouvez réellement utiliser IActionResult ou Task<IActionResult> au lieu de Thing ou Task<Thing> ou même Task<IEnumerable<Thing>>. Si vous avez une API qui renvoie du JSON , alors vous pouvez simplement faire ce qui suit:

[Route("api/[controller]")]
public class ThingsController : Controller
{
    // GET api/things
    [HttpGet]
    public async Task<IActionResult> GetAsync()
    {
    }

    // GET api/things/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetAsync(int id)
    {
        var thingFromDB = await GetThingFromDBAsync();
        if (thingFromDB == null)
            return NotFound();

        // Process thingFromDB, blah blah blah
        return Ok(thing); // This will be JSON by default
    }

    // POST api/things
    [HttpPost]
    public void Post([FromBody] Thing thing)
    {
    }
}

Mise à jour

Il semble que la préoccupation est que d'être explicite dans le retour de l'API est en quelque sorte utile, alors qu'il est possible d'être explicite , il est en fait pas très utile. Si vous êtes à l'écriture de tests unitaires que l'exercice de la requête / réponse pipeline que vous sont généralement vérifier le retour premières (ce qui serait le plus susceptible d'être JSON, c'est à dire; une chaîne de caractères en C#). Vous pouvez simplement prendre le chaîne retournée et convertir le fortement typé équivalent pour les comparaisons à l'aide de Assert.

Cela semble être la seule lacune avec l'aide d' IActionResult ou Task<IActionResult>. Si vous voulez vraiment, vraiment être explicite et vous souhaitez configurer le code de statut il y a plusieurs façons de le faire - mais il est mal vu que le cadre a déjà un mécanisme intégré pour cela, c'est à dire, l'utilisation de l' IActionResult de retour de la méthode des wrappers dans l' Controller classe. Vous pouvez écrire quelques personnalisé middleware pour gérer ce toutefois vous le souhaitez, cependant.

Enfin, je tiens à souligner que, si un appel API retourne null selon W3 un code d'état de 204 est en fait exacte. Pourquoi sur terre serait vous voulez un 404?

204

Le serveur a satisfait à la demande, mais n'a pas besoin de retourner un entité-corps, et pourrait vouloir retourner à jour de la métainformation. L' la réponse PEUT inclure de nouveaux ou mis à jour de la métainformation dans la forme de de l'entité en-têtes, qui si elle est présente, DOIT être associé à la demandé variante.

Si le client est un agent utilisateur, il ne DOIT PAS modifier son affichage du document à partir de ce qui a causé le demande à être envoyé. Cette réponse est principalement destiné à permettre l'entrée pour les mesures à prendre place sans ce qui entraîne une modification de l'utilisateur de l'agent actif de vue du document, bien que toute nouvelle ou mise à jour de la métainformation DOIT être appliqué au document actuellement dans le manuel de l'agent de la vue active.

La réponse 204 ne DOIT PAS inclure un message du corps, et est donc toujours résilié par la première ligne vide après les champs d'en-tête.

Je pense que la première phrase du deuxième paragraphe dit meilleur, "Si le client est un agent utilisateur, il ne DOIT PAS modifier son document de ce qui a causé le demande à être envoyé". C'est le cas avec une API. Par rapport à un 404:

Le serveur n'a pas trouvé quelque chose correspondant à l'URI de Demande. Pas de indication de savoir si la condition est temporaire ou permanente. Le 410 (Disparu) le code d'état DOIT être utilisé si le serveur sait, par l'intermédiaire de certains en interne configurable mécanisme, une vieille la ressource est indisponible de façon permanente et n'a pas d'adresse de transfert. Ce code d'état est couramment utilisé lorsque le serveur ne souhaite pas révéler exactement pourquoi la demande a été refusée, ou si aucun autre la réponse est applicable.

La principale différence étant l'un est plus applicable pour une API et l'autre pour l'affichage du document, c'est à dire; la page affichée.

3voto

Hackerman Points 10570

Afin d'accomplir quelque chose comme ça (quand même, je pense que la meilleure approche devrait être d'utiliser IActionResult ), vous pouvez suivre, où vous pouvez throw et HttpResponseException si votre Thing est null :

 // GET api/things/5
[HttpGet("{id}")]
public async Task<Thing> GetAsync(int id)
{
    Thing thingFromDB = await GetThingFromDBAsync();
    if(thingFromDB == null){
        throw new HttpResponseException(HttpStatusCode.NotFound); // This returns HTTP 404
    }
    // Process thingFromDB, blah blah blah
    return thing;
}
 

-1voto

Svek Points 6227

Ce que vous faites avec le retour des "Types Explicites" à partir du contrôleur n'est pas l'intention de coopérer avec votre exigence explicitement traiter avec votre propre code de réponse. La solution la plus simple est d'aller avec IActionResult (comme d'autres l'ont suggéré); Toutefois, vous pouvez également explicitement le contrôle de votre type de retour à l'aide de l' [Produces] filtre.

À l'aide de IActionResult.

Le chemin pour obtenir le contrôle sur les résultats de l'état, est vous avez besoin de retourner un IActionResult qui est l'endroit où vous pouvez ensuite profiter de l' StatusCodeResult type. Cependant, maintenant vous avez votre numéro de vouloir forcer un partiular format...

Les trucs ci-dessous est extrait du Document Microsoft: mise en forme des Données de Réponse -Forcer un Format Particulier

Forcer un Format Particulier

Si vous souhaitez restreindre la réponse formats spécifiques action, vous pouvez, vous pouvez appliquer l' [Produces] filtre. L' [Produces] filtre spécifie le format de réponse pour un l'action (ou contrôleur). Comme la plupart des Filtres, cela peut être appliqué à de l'action, de contrôleur ou de portée mondiale.

Mettre tous ensemble

Voici un exemple de contrôle sur l' StatusCodeResult en plus de la maîtrise de la "explicite le type de retour".

// GET: api/authors/search?namelike=foo
[Produces("application/json")]
[HttpGet("Search")]
public IActionResult Search(string namelike)
{
    var result = _authorRepository.GetByNameSubstring(namelike);
    if (!result.Any())
    {
        return NotFound(namelike);
    }
    return Ok(result);
}

Je ne suis pas un grand partisan de ce modèle, mais j'ai mis ces préoccupations dans certains autres réponses à d'autres questions. Vous aurez besoin de noter que l' [Produces] filtre exigera de vous d'une carte au Formateur / Sérialiseur / Type Explicite. Vous pouvez prendre un coup d'oeil à cette réponse pour plus d'idées ou de celui-ci pour la mise en œuvre d'un contrôle plus précis de votre ASP.NET de Base de l'API Web.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X