Exposer des services REST avec Nancy

LogoDans le premier article dédié à Nancy, nous avons vu comment installer Nancy et commencer à travailler avec. Dans ce deuxième article, nous allons voir comment exposer des services web REST et donc voir comment Nancy se pose comme une alternative viable à ASP.NET Web API.

Le modèle

Voici le modèle de classes avec lequel nous allons travailler :

public class Order
{
   public int Id { get; set; }
   public IEnumerable<OrderItem> OrderItems { get; set; }
   public Customer Customer { get; set; }

   public float TotalAmount
   {
      get
      {
         return OrderItems.Sum(orderItem => orderItem.Product.Price * orderItem.Quantity);
      }
   }
}

public class Customer
{
   public int Id { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

public class OrderItem
{
   public int Quantity { get; set; }
   public Product Product { get; set; }
}

public class Product
{
   public int Id { get; set; }
   public string Name { get; set; }
   public float Price { get; set; }
}

Pour résumer, nous avons des commandes, qui concernent chacune un client et des éléments de commande. Un élément de commande porte un produit et une quantité. On fait simple : pas de TVA, pas d’adresse d’expédition, etc. Le minimum.

Le module OrderModule

Nous allons maintenant ajouter un nouveau module, nommé OrderModule. Ce dernier aura pour but d’exposer des services dédiés à la gestion de commande d’un site de vente en ligne.

Nous voulons que toutes les routes qui concernent les commandes soient du type : http://localhost:7844/orders.

Nous voulons que la méthode Get sur la racine de notre module renvoie l’ensemble des commandes passées sur notre site. Voici donc la première version de notre module :

public class OrderModule : Nancy.NancyModule
{
   public OrderModule() : base("/orders")
   {
      IEnumerable<Order> allOrders = GetOrders();
      Get["/"] = _ => return allOrders;
   }

   private IEnumerable<Order> GetOrders()
   {
      ...
   }
}

Passons rapidement sur la méthode GetOrders() : sa place est dans un repository. Dans le constructeur, nous voyons que l’on renvoie brutalement le résultat renvoyé par GetOrders(). Et voilà ce que nous renvoie l’appel à notre service :

image

Tentons de faire abstraction du fait que John Doe achète une pelle, 5m de corde et une cadenas, ce qui peut paraître un peu louche, pour remarquer que notre service sait donc renvoyer une belle réponse au format JSON. Mais Nancy est plus fort que ça. On peut mieux contrôler ce que notre service retourne.

Content negotiation

On peut laisser Nancy s’occuper de la négociation de contenu à notre place. Et la plupart du temps, ça marche très bien. Mais on peut aussi vouloir garder plus de contrôle sur cette négociation de contenu.

Voici une deuxième version du constructeur de notre module :

public OrderModule() : base("/orders")
{
   IEnumerabl<Order> allOrders = GetOrders();
   Get["/"] = _ => Negotiate.WithStatusCode(HttpStatusCode.OK).WithModel(allOrders);
}

La classe Negociate expose une API fluent permettant de manipuler finement l'HttpResponse rendue par notre service. Dans notre cas, nous précisons que le code retour 200, pour signaler que tout va bien et que le modèle contiendra l’ensemble des commandes. Mais il est également possible d’agir sur l’ensemble des informations d’une HttpResponse : le type content, les cookies, le header, la vue ou les données retournées…

La récupération d’une commande en particulier

Nous allons maintenant ajouter une nouvelle route à notre module, permettant de retourner une commande en particulier, via son identifiant. Elle prend la forme suivante :

public OrderModule() : base("/orders")
{
   IEnumerable<Order> allOrders = GetOrders();
   Get["/"] = _ => Negotiate.WithStatusCode(HttpStatusCode.OK).WithModel(allOrders);
   Get["/{id}"] = parameters =>
   {
      try
      {
         var order = allOrders.First(o => o.Id == parameters.id);
         return Negotiate.WithStatusCode(HttpStatusCode.OK).WithModel(order);
      }
      catch (Exception)
      {
         return Negotiate.WithStatusCode(HttpStatusCode.NotFound);
      }
   };
}

La route Get["/{id}"] tente de trouver la commande ayant l’Id demandé, et rend soit cette commande, soit une erreur 404. Rien de très compliqué, mais quelques choses à noter :

  • parameters est dynamique. Il n’y a pas réellement de propriété parameters.id. Il faudra donc simplement s’assurer de la cohérence entre la route (Get["/{id}"] et les propriétés de la variable parameters.
  • La classe Negociate sert à rendre soit l’instance de la commande, soit la 404.

Le retour en recherchant la commande id=1 :

Id1

 

Le retour en recherchant une commande inconnue avec id=666 :

Id666Wouhou ! Une 404. Et en bon framework, Nancy supporte tous les verbes Http :


Put["/"] = _ => { /*do stuff */ };
Delete["/"] = _ => { /*do stuff */ };
Post["/"] = _ => { /*do stuff */ };
Options["/"] = _ => { /*do stuff */ };

Tester mes services

Nancy permet de tester très facilement ses services. Nous allons donc créer un projet NancyDemo.Tests, et lui ajouter le package Nancy.Testing. Cela nous permet d'écrire des tests unitaires de la forme suivante :


[TestMethod]
public void when_i_grab_a_bad_order_I_get_404()
{
   //Arrange
   var browser = new Browser(m => m.Module<OrderModule>());

   //Act
   var response = browser.Get("/orders/666", with =>
   {
      with.HttpRequest();
      with.Header("accept", "application/json");
   });

   //Assert
   Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
}

L'API expose une classe Browser, qui nous permet de simuler... un navigateur (eh ouais). Et de la même façon que plus haut avec un outil du type DHC pour Chrome, nous pouvons générer une HttpRequest de la forme voulue, avec le verbe voulu et vérifier l'HttpResponse. Il est également possible de naviguer dans le corps de la réponse et de valider qu'il contient les données attendues.

Et voilà pour les services ! Dans le prochain article, nous verrons comment Nancy permet d'exposer des pages Web.