web-dev-qa-db-fra.com

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

Les contrôleurs d'API ASP.NET Core renvoient généralement des types explicites (et le font par défaut si vous créez un nouveau projet), par exemple:

[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 que return null; - il retourne un HTTP 204: succès, pas de contenu.

Ceci est alors considéré par beaucoup de composants Javascript côté client comme un succès, il y a donc 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 (telle que cette question et cette réponse ) pointe vers des méthodes d'extension return NotFound(); utiles pour le contrôleur, mais toutes ces méthodes renvoient IActionResult, ce qui n'est pas compatible avec mon type de retour Task<Thing>. Ce 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 l'utiliser, le type de retour de GetAsync doit être changé en Task<IActionResult> - le typage explicite est perdu, et tous les types de retour sur le contrôleur doivent être modifiés (c'est-à-dire ne pas utiliser de typage explicite) ou mélangez où certaines actions traitent de types explicites tandis que d'autres. De plus, les tests unitaires doivent maintenant faire des hypothèses sur la sérialisation et désérialiser explicitement le contenu de la variable IActionResult où ils avaient auparavant un type concret.

Il existe de nombreuses façons de contourner ce problème, mais il semble que ce soit un méli-mélo déroutant qui pourrait facilement être conçu. La vraie question est donc: quelle est la bonne façon envisagée par les concepteurs ASP.NET Core?

Il semble que les options possibles sont:

  1. Avoir un mélange étrange (difficile à tester) de types explicites et IActionResult en fonction du type attendu.
  2. Oubliez les types explicites, ils ne sont pas vraiment supportés par Core MVC, always utilise IActionResult (dans quel cas pourquoi sont-ils présents?)
  3. Ecrivez une implémentation de HttpResponseException et utilisez-la comme ArgumentOutOfRangeException (voir cette réponse pour une implémentation). Cependant, cela nécessite l'utilisation d'exceptions pour le déroulement du programme, ce qui est généralement une mauvaise idée et également déconseillé par l'équipe principale de MVC .
  4. Ecrivez une implémentation de HttpNoContentOutputFormatter qui renvoie 404 pour les demandes GET.
  5. Quelque chose d'autre me manque dans la façon dont le Core MVC est censé fonctionner?
  6. Ou y a-t-il une raison pour laquelle 204 est correct et 404 incorrect pour une requête GET échouée?

Celles-ci impliquent toutes des compromis et des refactorisations qui perdent quelque chose ou ajoutent ce qui semble être une complexité inutile en contradiction avec la conception de MVC Core. Quel compromis est le bon et pourquoi?

35
Keith

Ceci est traité dans ASP.NET Core 2.1 avec ActionResult<T>:

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

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

    return thing;
}

Ou même:

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

Je mettrai à jour cette réponse avec plus de détails une fois que je l'aurai mise en œuvre.

Réponse originale

Dans ASP.NET Web API 5, il y avait une HttpResponseException (comme l'a souligné Hackerman ), mais elle a été supprimée de Core et aucun middleware ne le permet.

Je pense que ce changement est dû au .NET Core - où ASP.NET essaie de tout faire à l’origine, ASP.NET Core ne fait que ce que vous lui dites spécifiquement (ce qui explique en grande partie pourquoi il est tellement plus rapide et portable ).

Je ne trouve pas de bibliothèque existante qui le fasse, alors je l’ai écrite 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 gestionnaire RequestDelegate qui vérifie la nouvelle exception et la convertit en code de statut de réponse HTTP:

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 ce middleware dans notre Startup.Configure:

public class Startup
{
    ...

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

Enfin, les actions peuvent générer une exception de code d'état HTTP, tout en renvoyant un type explicite pouvant facilement être testé par unité sans conversion de IActionResult:

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

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

    return thing;
}

Cela conserve les types explicites pour les valeurs de retour et permet de faire facilement la distinction entre les résultats vides réussis (return null;) et une erreur car quelque chose ne peut pas être trouvé (je pense que c'est comme jeter une ArgumentOutOfRangeException).

Bien que ce soit une solution au problème, cela ne répond toujours pas vraiment à ma question: les concepteurs de l’API Web prennent en charge les types explicites dans l’espoir de les utiliser. Ils ont ajouté une gestion spécifique pour return null; afin qu’elle produise une plutôt que 200, et puis n'a pas ajouté un moyen de traiter 404? Il semble que beaucoup de travail est nécessaire pour ajouter quelque chose d'aussi fondamental.

36
Keith

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 retourne JSON, vous pouvez simplement procéder comme 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)
    {
    }
}

Mettre à jour

Il semble que la préoccupation est qu'être explicite dans le retour d'une API est utile, alors qu'il est possible d'être explicite, ce n'est en fait pas très utile. Si vous écrivez des tests unitaires qui exercent le pipeline de demandes/réponses, vous allez généralement vérifier le retour brut (ce qui serait probablement JSON, c'est-à-dire; une chaîne dans C #). Vous pouvez simplement prendre la chaîne renvoyée et la reconvertir en équivalent fortement typé pour les comparaisons à l'aide de Assert

Cela semble être la seule lacune à utiliser IActionResult ou Task<IActionResult>. Si vous voulez vraiment, vraiment, être explicite et que vous voulez tout de même définir le code de statut, il existe plusieurs façons de le faire - mais cela est mal vu, car la structure a déjà un mécanisme intégré pour cela, c'est-à-dire; en utilisant les wrappers de méthode retournant IActionResult dans la classe Controller. Vous pouvez écrire quelques middleware personnalisé pour gérer cela comme vous le souhaitez.

Enfin, je voudrais souligner que si un appel d'API renvoie null conformément à W3, un code d'état de 204 est réellement exact. Pourquoi voudriez-vous un 404?

204

Le serveur a répondu à la demande mais n'a pas besoin de renvoyer un entité-corps, et peut vouloir renvoyer la métainformation mise à jour. Le response PEUT inclure des métainformations nouvelles ou mises à jour sous la forme de les en-têtes d'entité, qui, le cas échéant, DEVRAIENT être associés au variante demandée.

Si le client est un agent d'utilisateur, il NE DEVRAIT PAS changer sa vue de document de celui qui a provoqué l'envoi de la demande. Cette réponse est principalement destiné à permettre la saisie d’actions sans que provoquant une modification de la vue de document active de l'agent d'utilisateur, bien que toute métainformation nouvelle ou mise à jour DEVRAIT être appliquée au document actuellement dans la vue active de l'agent d'utilisateur.

La réponse 204 NE DOIT PAS inclure de corps de message et est donc toujours terminé 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 le dit mieux: "Si le client est un agent d'utilisateur, il NE DEVRAIT PAS changer sa vue de document de celle qui a provoqué l'envoi de la demande". C'est le cas avec une API. Par rapport à un 404:

Le serveur n'a rien trouvé qui corresponde à l'URI de demande. Non il est indiqué si la condition est temporaire ou permanent. Le code d'état 410 (Gone) DEVRAIT être utilisé si le serveur sait, grâce à un mécanisme configurable en interne, qu’un ancien la ressource est indisponible en permanence et n'a pas d'adresse de transfert . Ce code de statut est couramment utilisé lorsque le serveur ne souhaite pas révéler exactement pourquoi la demande a été refusée, ou quand aucun autre la réponse est applicable.

La différence principale étant l’une s’applique davantage à une API et l’autre à la vue Document, c’est-à-dire .; la page affichée.

14
David Pine

Afin d'accomplir quelque chose comme ça (encore, je pense que la meilleure approche devrait 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;
}
2
Hackerman

En renvoyant "Types explicites" depuis le contrôleur, vous n'allez pas coopérer avec votre exigence de traiter explicitement votre propre code de réponse. La solution la plus simple est d’utiliser IActionResult (comme certains l’ont suggéré); Cependant, vous pouvez également contrôler explicitement votre type de retour à l'aide du filtre [Produces].

Utilisation de IActionResult.

Pour obtenir le contrôle des résultats de statut, vous devez renvoyer une variable IActionResult, qui vous permet de tirer parti du type StatusCodeResult. Cependant, vous avez maintenant votre question de vouloir imposer un format particulier ...

Les informations ci-dessous sont extraites du document Microsoft: Mise en forme des données de réponse - Lancement d’un format particulier

Forcer un format particulier

Si vous souhaitez restreindre les formats de réponse pour un fichier .__ spécifique. action possible, vous pouvez appliquer le filtre [Produces]. Le Le filtre [Produces] spécifie les formats de réponse pour un fichier .__ spécifique. action (ou contrôleur). Comme la plupart des Filtres , ceci peut être appliqué à l'action, le contrôleur ou la portée globale.

Mettre tous ensemble

Voici un exemple de contrôle sur la StatusCodeResult en plus du contrôle sur le "type de retour explicite".

// 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 de conception, mais j'ai apporté ces préoccupations dans certaines réponses supplémentaires aux questions d'autres personnes. Vous devrez noter que le filtre [Produces] vous demandera de le mapper au formateur/sérialiseur/type explicite approprié. Vous pouvez consulter cette réponse pour plus d’idées ou celle-ci pour mettre en œuvre un contrôle plus granulaire de votre API Web ASP.NET Core .

0
Svek