Retour d'un 404 d'une saisie explicite ASP.NET controller de L'API de base (non IActionResult))

ASP.NET les contrôleurs D'API de base renvoient généralement des types explicites (et le font 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 que return null; - il renvoie 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, donc il y a du 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 ce question et cette réponse ) pointe à des méthodes d'extension utiles return NotFound(); pour le contrôleur, mais tous ces retour IActionResult , 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);
}

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

il y a des tas de façons de contourner cela, mais il semble qu'il s'agisse d'un contresens confus qui pourrait facilement être conçu, donc la vraie question Est: qu'est-ce que le de la bonne façon voulue par le ASP.NET des designers de base?

il semble que les options possibles sont:

  1. ont un mélange bizarre (salissant à tester) 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 utilisez IActionResult (dans ce cas, pourquoi sont-ils présents?)
  3. écrire une implémentation de HttpResponseException et l'utiliser comme ArgumentOutOfRangeException (voir cette réponse pour une implémentation). Cependant, cela nécessite d'utiliser des exceptions pour le flux de programme, qui est généralement une mauvaise idée et aussi déprécié par l'équipe de base MVC .
  4. écrire une implémentation de HttpNoContentOutputFormatter qui renvoie 404 pour les requêtes GET.
  5. quelque chose d'autre que je ne vois pas MVC de base est censé fonctionner?
  6. Ou est-il une raison pour laquelle 204 est correct, et 404 mauvais pour l'échec d'une requête GET?

tout cela implique des compromis et un remaniement qui perdent quelque chose ou ajoutent ce qui semble être une complexité inutile en contradiction avec la conception du cœur MVC. Quel compromis est le bon et pourquoi?

27
demandé sur Community 2017-01-04 16:03:48

4 réponses

c'est ASP.NET noyau 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 implémentée.

Réponse Originale

In ASP.NET Web API 5 Il y avait un HttpResponseException (comme souligné par Hackerman ) mais il a été retiré du noyau et il n'y a pas middleware pour le gérer.

je pense que ce changement est dû à .net Core-où ASP.NET essaie de tout faire hors de la boîte, ASP.NET Core ne fait que ce que vous lui dites spécifiquement (ce qui est une grande partie de la raison pour laquelle il est tellement plus rapide et portable).

Je ne trouve pas de bibliothèque existante qui fasse cela, donc je l'ai écrite moi-même. Tout d'abord, nous avons besoin d'une exception personnalisée à vérifier pour:

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

    public HttpStatusCode StatusCode { get; set; }
}

alors nous avons besoin d'un RequestDelegate handler 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();
        }
    }
}

puis nous enregistrons cet middleware dans notre Startup.Configure :

public class Startup
{
    ...

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

enfin les actions peuvent jeter l'exception du code de statut HTTP, tout en retournant un type explicite qui peut facilement être testé à l'unité sans conversion de IActionResult :

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

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

    return thing;
}

cela garde les types explicites pour les valeurs de retour et permet une distinction facile entre les résultats vides réussis ( return null; ) et une erreur parce que quelque chose ne peut pas être trouvé (je pense à lui comme jeter un ArgumentOutOfRangeException ).

bien qu'il s'agisse D'une solution au problème, il ne répond toujours pas vraiment à ma question - les concepteurs de l'API Web construire soutien pour les types explicites avec l'espoir qu'ils seraient utilisés, ajouté manipulation spécifique pour return null; afin qu'il produirait un 204 plutôt que 200, et puis vous n'avez rien ajouté pour gérer 404? Cela semble être beaucoup de travail d'ajouter quelque chose de si basique.

27
répondu Keith 2018-02-02 18:24:03

vous pouvez en fait 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 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 explicite dans le retour d'une API est d'une certaine manière utile, alors qu'il est possible d'être explicite en fait, c'est pas très utile. Si vous écrivez des tests unitaires qui exercent le pipeline de demande / réponse , vous allez généralement vérifier le retour brut (qui serait très probablement JSON , c.-à-d. une chaîne dans c# ). Vous pouvez simplement prendre la chaîne retournée et la convertir de nouveau à l'équivalent fortement dactylographié pour des comparaisons en utilisant Assert .

cela semble être le seul défaut d'utilisation de IActionResult ou Task<IActionResult> . Si vous voulez vraiment, vraiment être explicite et que vous voulez encore définir le code de statut, il y a plusieurs façons de le faire - mais il est mal vu car le framework a déjà un mécanisme intégré pour cela, c'est-à-dire en utilisant la méthode de retour IActionResult dans la classe Controller . Vous pouvez écrire quelques middleware personnalisé pour gérer cela comme vous le souhaitez, cependant.

enfin, je voudrais comme pour souligner que si un appel API retourne null selon W3 un code d'état de 204 est en fait exact. Pourquoi diable voudriez-vous un 404 ?

204

le serveur a rempli la requête mais n'a pas besoin de retourner entité-corps, et pourrait vouloir retourner à jour de la métainformation. Le la réponse PEUT inclure des méta-informations nouvelles ou mises à jour sous la forme de entités-en-têtes qui, s'ils sont présents, devraient être associés à la demandé variante.

si le client est un agent utilisateur, il ne doit pas changer sa vue de document de ce qui a provoqué l'envoi de la demande. Cette réponse est principalement destiné à permettre la saisie des données pour que les actions aient lieu sans modifier la vue active du document de l'agent utilisateur, bien que toute information nouvelle ou mise à jour doit être appliqué au document actuellement dans la vue active de l'agent utilisateur.

la réponse 204 ne doit pas inclure un corps de message, 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 le dit mieux,"si le client est un agent utilisateur, il ne doit pas changer son point de vue sur le document par rapport à celui qui a provoqué l'envoi de la demande". C'est le cas avec une API. Comme par rapport à un 404 :

le serveur n'a rien trouvé qui corresponde à L'URI de la requête. Aucun indication de savoir si la condition est temporaire ou permanent. Le code d'état 410 (Gone) doit ê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 réexpédition. 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 que l'une s'applique davantage à une API et l'autre à la vue document, c.-à-d. la page affichée.

12
répondu David Pine 2017-01-09 13:29:00

afin d'accomplir quelque chose comme cela (encore, je pense que la meilleure approche devrait utiliser IActionResult ), vous pouvez suivre, où vous pouvez throw an 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;
}
1
répondu Hackerman 2017-01-04 20:17:33

ce que vous faites avec le retour " types explicites " du contrôleur n'est pas va coopérer avec votre exigence de traiter explicitement avec votre propre code de réponse . La solution la plus simple est d'aller avec IActionResult (comme d'autres l'ont suggéré); cependant, vous pouvez aussi explicitement contrôler votre type de retour en utilisant le filtre [Produces] .

utilisant IActionResult .

la façon d'obtenir le contrôle sur les résultats d'état, est que vous devez retourner un IActionResult qui est où vous pouvez alors profiter du type StatusCodeResult . Cependant, maintenant vous avez votre problème de vouloir forcer un format partiulaire...

le contenu ci-dessous est tiré du document de Microsoft: formatage des données de réponse-forçage d'un Format particulier

forçant une Format Particulier

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

Mise au point

Voici un exemple de contrôle sur le 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 mis ces préoccupations dans quelques réponses supplémentaires aux questions des autres. Vous devrez noter que le filtre [Produces] vous demandera de l'associer au type de Formatteur / sérialiseur / explicite approprié. Vous pouvez jeter un oeil à cette réponse pour plus idées ou celui-ci pour mettre en œuvre un contrôle plus granulaire sur votre ASP.NET Core Web API .

0
répondu Svek 2017-05-23 12:10:11