35 votes

Ressources imbriquées dans ASP.net MVC 4 WebApi

Y a-t-il une meilleure façon de gérer les ressources imbriquées dans le nouveau WebApi ASP.net MVC 4 que de créer une route spéciale pour chacune d'entre elles ? (similaire à ici : ASP.Net MVC prend en charge les ressources imbriquées ? - ceci a été publié en 2009).

Par exemple, je veux gérer :

/customers/1/products/10/

J'ai vu quelques exemples de ApiController actions nommées autres que Get() , Post() etc., par exemple aquí Je vois un exemple d'une action appelée GetOrder() . Je ne trouve cependant aucune documentation à ce sujet. Y a-t-il un moyen d'y parvenir ?

37voto

Teto Points 585

Désolé, j'ai mis à jour cette page plusieurs fois car je suis en train de trouver une solution.

Il semble qu'il y ait plusieurs façons d'aborder cette question, mais la plus efficace que j'ai trouvée jusqu'à présent est la suivante :

Ajoutez-le sous la route par défaut :

routes.MapHttpRoute(
    name: "OneLevelNested",
    routeTemplate: "api/{controller}/{customerId}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Cette route correspondra ensuite à toute action du contrôleur et au nom du segment correspondant dans l'URL. Par exemple :

/api/customers/1/orders correspondra :

public IEnumerable<Order> Orders(int customerId)

/api/customers/1/orders/123 correspondra :

public Order Orders(int customerId, int id)

/api/customers/1/products correspondra :

public IEnumerable<Product> Products(int customerId)

/api/customers/1/products/123 correspondra :

public Product Products(int customerId, int id)

Le nom de la méthode doit correspondre au segment {action} spécifié dans l'itinéraire.


Note importante :

Des commentaires

Depuis le RC, vous devrez indiquer à chaque action les types de verbes qui sont acceptables, c'est à dire [HttpGet] etc.

10voto

Loudenvier Points 2390

EDIT : Bien que cette réponse soit toujours valable pour l'API Web 1, pour l'API Web 2, je conseille vivement d'utiliser La réponse de Daniel Halan car il s'agit de l'état de l'art pour le mappage des sous-ressources (entre autres subtilités).


Certaines personnes n'aiment pas utiliser {action} dans les API Web parce qu'elles pensent que, ce faisant, elles brisent l'"idéologie" REST... Je soutiens que. {action} est simplement une construction qui aide au routage. Elle est interne à votre implémentation et n'a rien à voir avec le verbe HTTP utilisé pour accéder à un fichier ressource .

Si vous imposez des contraintes de verbe HTTP aux actions et que vous les nommez en conséquence, vous n'enfreignez aucune directive RESTful et vous vous retrouverez avec des contrôleurs plus simples et plus concis au lieu de tonnes de contrôleurs individuels pour chaque sous-ressource. N'oubliez pas : l'action n'est qu'un mécanisme de routage, et elle est interne à votre mise en œuvre. Si vous vous heurtez au framework, c'est que quelque chose ne va pas, soit avec le framework, soit avec votre implémentation. Il suffit de mapper l'itinéraire avec une contrainte HTTPMETHOD pour que tout aille bien :

routes.MapHttpRoute(
    name: "OneLevelNested",
    routeTemplate: "api/customers/{customerId}/orders/{orderId}",
    constraints: new { httpMethod = new HttpMethodConstraint(new string[] { "GET" }) },
    defaults: new { controller = "Customers", action = "GetOrders", orderId = RouteParameter.Optional,  }
);

Vous pouvez les gérer dans le CustomersController de la manière suivante :

public class CustomersController
{
    // ...
    public IEnumerable<Order> GetOrders(long customerId)
    {
        // returns all orders for customerId!
    }
    public Order GetOrders(long customerId, long orderId)
    {
        // return the single order identified by orderId for the customerId supplied
    }
    // ...
}

Vous pouvez également acheminer une action Créer sur la même "ressource" (commandes) :

routes.MapHttpRoute(
    name: "OneLevelNested",
    routeTemplate: "api/customers/{customerId}/orders",
    constraints: new { httpMethod = new HttpMethodConstraint(new string[] { "POST" }) },
    defaults: new { controller = "Customers", action = "CreateOrder",  }
);

Et le traiter en conséquence dans le contrôleur du client :

public class CustomersController
{
    // ...
    public Order CreateOrder(long customerId)
    {
        // create and return the order just created (with the new order id)
    }
    // ...
}

Oui, vous devez toujours créer beaucoup de routes, car l'API Web ne peut toujours pas diriger vers différentes méthodes en fonction du chemin... Mais je pense qu'il est plus propre de définir les routes de manière déclarative que d'inventer des mécanismes de répartition personnalisés basés sur des enums ou d'autres astuces.

Pour le consommateur de votre API, cela aura l'air parfaitement RESTful :

GET http://your.api/customers/1/orders (correspond à GetOrders(long) qui renvoie toutes les commandes du client 1)

GET http://your.api/customers/1/orders/22 (correspond à GetOrders(long, long) qui renvoie la commande 22 du client 1)

POST http://your.api/customers/1/orders (correspond à CreateOrder(long) qui créera un ordre et le retournera à l'appelant (avec le nouvel ID qui vient d'être créé).

Mais ne prenez pas ma parole comme une vérité absolue. Je suis encore en train de l'expérimenter et je pense que MS n'a pas réussi à traiter correctement l'accès aux sous-ressources.

Je vous invite à essayer http://www.servicestack.net/ pour une expérience moins douloureuse d'écriture d'apis REST... Mais ne vous méprenez pas, j'adore les API Web et je les utilise pour la plupart de mes projets professionnels, principalement parce qu'il est plus facile de trouver des programmeurs qui les "connaissent" déjà... Pour mes projets personnels, je préfère ServiceStack.

8voto

Daniel Halan Points 180

Depuis l'API Web 2, vous pouvez utiliser les attributs de route pour définir un routage personnalisé par méthode, ce qui permet un routage hiérarchique.

public class CustomersController : ApiController
{
    [Route("api/customers/{id:guid}/products")]
    public IEnumerable<Product> GetCustomerProducts(Guid id) {
       return new Product[0];
    }
}

Vous devez également initialiser le mappage d'attributs dans WebApiConfig.Register(),

  config.MapHttpAttributeRoutes();

5voto

mo. Points 1094

Je n'aime pas utiliser le concept d'"actions" dans le parcours d'une API Web ASP.NET. L'action dans REST est censée être le verbe HTTP. J'ai mis en œuvre ma solution d'une manière assez générique et assez élégante en utilisant simplement le concept de contrôleur parent.

https://stackoverflow.com/a/15341810/326110

Vous trouverez ci-dessous la réponse reproduite dans son intégralité, car je ne sais pas trop quoi faire lorsqu'un seul message répond à deux questions de l'OS :(


Je voulais gérer cela d'une manière plus générale, au lieu de câbler directement un ChildController avec controller = "Child" comme l'a fait Abhijit Kadam. J'ai plusieurs contrôleurs enfant et je ne voulais pas avoir à tracer une route spécifique pour chacun d'entre eux, avec controller = "ChildX" y controller = "ChildY" encore et encore.

Mon WebApiConfig ressemble à ça :

config.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional }
);
  config.Routes.MapHttpRoute(
  name: "ChildApi",
  routeTemplate: "api/{parentController}/{parentId}/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional }
);

Mes contrôleurs parents sont très standard, et correspondent à la route par défaut ci-dessus. Un exemple de contrôleur enfant ressemble à ceci :

public class CommentController : ApiController
{
    // GET api/product/5/comment
    public string Get(ParentController parentController, string parentId)
    {
        return "This is the comment controller with parent of "
        + parentId + ", which is a " + parentController.ToString();
    }
    // GET api/product/5/comment/122
    public string Get(ParentController parentController, string parentId,
        string id)
    {
        return "You are looking for comment " + id + " under parent "
            + parentId + ", which is a "
            + parentController.ToString();
    }
}
public enum ParentController
{
    Product
}

Quelques inconvénients de mon implémentation

  • Comme vous pouvez le voir, j'ai utilisé un enum Je dois donc toujours gérer les contrôleurs parents à deux endroits différents. Il aurait pu tout aussi bien s'agir d'un paramètre de type chaîne de caractères, mais je voulais éviter que le paramètre api/crazy-non-existent-parent/5/comment/122 de travailler.
  • Il y a probablement un moyen d'utiliser la réflexion ou autre pour faire cela à la volée sans avoir à le gérer séparément, mais cela fonctionne pour moi pour le moment.
  • Il ne soutient pas les enfants des enfants.

Il existe probablement une meilleure solution, encore plus générale, mais comme je l'ai dit, cela fonctionne pour moi.

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