Service REST d'écho en C#, sans cadriciel

Il existe plusieurs cadriciels (Frameworks) en C# pour faciliter l'exposition de services Web de type REST. Cependant, utiliser un cadriciel tend à masquer les détails, et à obscurcir la compréhension de ce qui se passe vraiment.

L'exercice qui suit vise donc à mettre en place « manuellement » un service Web de type REST. Le service sera simple, mais la mécanique sera pleinement exposée et, souhaitons-le, le tout sera moins « magique » si vous utilisez un cadriciel par la suite pour en arriver à faire une tâche semblable.

Merci à Nicolas Chourot pour avoir écrit la version initiale de cet exemple.

Vous voudrez probablement installer Postman pour tester votre implémentation : https://www.postman.com/

Idée générale

Le service que nous exposerons sera un simple service d'écho, qui acceptera des requêtes http sur un port et retournera la charge utile (le Payload) de cette requête au client.

Code

L'implémentation (très naive) suit.

Le programme sera un serveur acceptant des requêtes http. Nous nous limiterons délibérément à des outils standards de la plateforme .NET, et nous n'aurons recours à aucun cadriciel par souci de simplicité.

using System;
using System.IO;
using System.Text;
using System.Net;
using System.Threading;

namespace HttpServer
{
   class Program
   {

Nous accepterons les requêtes http sur le port PORT. Ceci passe par l'ajout de préfixes à la propriété Prefixes d'un HttpListener (voir la documentation de cette classe pour comprendre le * dans "*:2357" et voir les alternatives possibles).

      const int PORT = 2357;
      static HttpListener StartServer()
      {
         var listener = new HttpListener();
         listener.Prefixes.Add($"http://*:{PORT}/");
         listener.Start();
         Console.WriteLine($"En attente sur le port {PORT}");
         return listener;
      }

La méthode SendText(response,text,encoding) émet le texte text à travers l'objet response destiné au client. C'est un simple service d'écho.

      static void SendText(HttpListenerResponse response, string text, Encoding encoding)
      {
         byte[] buffer = Encoding.GetEncoding(encoding.CodePage).GetBytes(text);
         response.ContentLength64 = buffer.Length;
         using (var os = response.OutputStream)
            os.Write(buffer, 0, buffer.Length);
      }

La méthode GetText(request) extrait le texte d'une requête http, identifie son encodage d'origine, et retourne cette paire d'informations à l'appelant.

On peut avantageusement (en général; C# n'en profitera peut-être pas) remplacer l'alternative ici (le if... else...) par une expression ternaire :

return (request.ContentType != null? reader.ReadToEnd() : "", encoding);
      static (string,Encoding) GetText(HttpListenerRequest request)
      {
         using (Stream body = request.InputStream)
         {
            var encoding = request.ContentEncoding;
            using (var reader = new StreamReader(body, encoding))
               if (request.ContentType != null)
                  return (reader.ReadToEnd(), encoding);
               else
                  return ("", encoding);
         }
      }

Le programme principal est tout simple : il consomme chaque requête et lui fait écho dans son encodage d'origine. L'appel à GetContext bloque jusqu'à réception d'une requête http, puis les fonctions GetText et SendText se chargent de l'écho.

Notez qu'une version asynchrone aurait été avantageuse, facilitant l'interruption élégante de l'attente.

Ce serveur est simpliste, mais on peut ajouter un traitement arbitrairement complexe entre la consommation de la requête et la réception d'un résultat :

// ...
var (text, encoding) = GetText(context.Request);
text = TraitementSouhaité(text); // ICI
SendText(context.Response, text, encoding);
// ...

Évidemment, on peut aussi (c'est l'idéal) utiliser la charge utile de context.Request, extraire le contenu (JSON, XML, autre) et faire des manipulations sur ce dernier, préservant une sémantique plus riche que si on se limite à manipuler son équivalent « texte ».

      static void Main()
      {
         bool fini = false;
         var listener = StartServer();
         var th = new Thread(() =>
         {
            Console.ReadKey(true);
            fini = true;
         });
         th.Start();
         while (!fini)
         {
            var context = listener.GetContext();
            var (text, encoding) = GetText(context.Request);
            SendText(context.Response, text, encoding);
         }
         th.Join();
      }
   }
}

Valid XHTML 1.0 Transitional

CSS Valide !