Tiny.RestClient : le client REST pour consommer vos API

Tiny.RestClient : le client REST pour consommer vos API

Je suis très heureux de vous présenter un projet sur lequel je travaille depuis un petit moment : un client REST compatible .NetStandard entièrement fluent.

Le code est disponible sur Github.
Et la librairie est sur nuget.
Vous voulez en parler ? Passez me voir sur Gitter

Le but principal de client est d’avoir un client REST optimisé et simple à utiliser.
Il est compatible .Net Standard 1.3 et 2.0 ce qui signifie qu’il est utilisable sur le .Net Framework 4.5 ou supérieur, Xamarin et .Net Core et UWP.

Fonctionnalités :
* Un client moderne pour faire des appels asynchrones pour les API REST
* Support des principaux verbes HTTP : GET, POST , PUT, DELETE, PATCH
* Support des verbes personnalisés
* Support des “cancellation token” sur tout type de requêtes
* Détection automatique du déserialiseur à utiliser
* Support de la sérialisation/ déserisalisation JSON et XML
* Support de sérialiseur / déserialiseur personnalisés
* Support du multi-part form (envoi multiples de fichiers)
* Appels http optimisés
* Exceptions typés plus facile interprétés
* Fournit une façon simple de logger toutes les requêtes : les réponses qui échouent, le temps de réponses…
* Possibilité de définir le timeout globalement ou par requête
* L’API lance une Timeout exception lorsque les requêtes sont en timeout (par défaut HttpClient lance une exception OperationCancelledException ce qui rend impossible de faire la différence entre une annulation du token et un timeout)
* Permet d’exporter les requêtes sous forme de collection Postman

Créer un client

using Tiny.RestClient;
var client = new TinyRestClient("http://MyAPI.com/api", new HttpClient());

Headers

Ajouter un header par défaut pour toutes les requêtes

// Add default header for each calls
client.Settings.DefaultHeaders.Add("Token", "MYTOKEN");

Ajouter un header pour la requête courante

// Add header for this request only
client.GetRequest("City/All").
      AddHeader("Token", "MYTOKEN").
      ExecuteAsync();

Lire les header de la réponse

await client.GetRequest("City/All").
             FillResponseHeaders(out headersOfResponse Headers).
             ExecuteAsync();
foreach(var header in headersOfResponse)
{
    Debug.WriteLine($"{current.Key}");
    foreach (var item in current.Value)
    {
        Debug.WriteLine(item);
    }
}

Créer une requête GET

var cities = client.GetRequest("City/All").ExecuteAsync<List<City>>();
// GET http://MyAPI.com/api/City/All an deserialize automaticaly the content

// Add a query parameter
var cities = client.
    GetRequest("City").
    AddQueryParameter("id", 2).
    AddQueryParameter("country", "France").
    ExecuteAsync<City>> ();
// GET http://MyAPI.com/api/City?id=2&country=France and deserialize automaticaly the content

Créer une requête POST

// POST
 var city = new City() { Name = "Paris" , Country = "France"};

// With content
var response = await client.PostRequest("City", city).
                ExecuteAsync<bool>();
// POST http://MyAPI.com/api/City with city as content

// With form url encoded data
var response = await client.
                PostRequest("City/Add").
                AddFormParameter("country", "France").
                AddFormParameter("name", "Paris").
                ExecuteAsync<Response>();
// POST http://MyAPI.com/api/City/Add with from url encoded content


var fileInfo = new FileInfo("myTextFile.txt");
var response = await client.
                PostRequest("City/Image/Add").
                AddFileContent(fileInfo, "text/plain").
                ExecuteAsync<Response>();
// POST text file at http://MyAPI.com/api/City/Add 

Créer une requête avec un verbe http personalisé

 await client.
       NewRequest(new System.Net.Http.HttpMethod("HEAD"), "City").
       ExecuteAsync();

Définir le timeout

Définir un timeout global

client.Settings.DefaultTimeout = TimeSpan.FromSeconds(100);

Définir le timeout pour sur une requête

request.WithTimeout(TimeSpan.FromSeconds(100));

Télécharger un fichier

string filePath = "c:\map.pdf";
FileInfo fileInfo = await client.
                GetRequest("City/map.pdf").
                DownloadFileAsync("c:\map.pdf");
// GET http://MyAPI.com/api/City/map.pdf 

Récupérer une réponse HttpResponseMessage brute

var response = await client.
                PostRequest("City/Add").
                AddFormParameter("country", "France").
                AddFormParameter("name", "Paris").
                ExecuteAsHttpResponseMessageAsync();
// POST http://MyAPI.com/api/City/Add with from url encoded content

Lire une réponse en tant que String

string response = await client.
                GetRequest("City/All").
                ExecuteAsStringAsync();

Effectuer des requêtes multi part

Créer des requêtes multipart est très simple avec le RestClient.

// With 2 json content
var city1 = new City() { Name = "Paris" , Country = "France"};
var city2 = new City() { Name = "Ajaccio" , Country = "France"};
var response = await client.NewRequest(HttpVerb.Post, "City").
await client.PostRequest("MultiPart/Test").
              AsMultiPartFromDataRequest().
              AddContent<City>(city1, "city1", "city1.json").
              AddContent<City>(city2, "city2", "city2.json").
              ExecuteAsync();


// With 2 byte array content
byte[] byteArray1 = ...
byte[] byteArray2 = ...           
              
await client.PostRequest("MultiPart/Test").
              AsMultiPartFromDataRequest().
              AddByteArray(byteArray1, "request", "request2.bin").
              AddByteArray(byteArray2, "request", "request2.bin")
              ExecuteAsync();
  

// With 2 streams content        
Stream1 stream1 = ...
Stream stream2 = ...         
await client.PostRequest("MultiPart/Test").
              AsMultiPartFromDataRequest().
              AddStream(stream1, "request", "request2.bin").
              AddStream(stream2, "request", "request2.bin")
              ExecuteAsync();
              
              
// With 2 files content           

var fileInfo1 = new FileInfo("myTextFile1.txt");
var fileInfo2 = new FileInfo("myTextFile2.txt");

var response = await client.
                PostRequest("City/Image/Add").
                AsMultiPartFromDataRequest().
                AddFileContent(fileInfo1, "text/plain").
                AddFileContent(fileInfo2, "text/plain").
                ExecuteAsync<Response>();

// With mixed content                  
await client.PostRequest("MultiPart/Test").
              AsMultiPartFromDataRequest().
              AddContent<City>(city1, "city1", "city1.json").
              AddByteArray(byteArray1, "request", "request2.bin").
              AddStream(stream2, "request", "request2.bin")
              ExecuteAsync();

Streams et byte[]

Vous pouvez faire des requêtes avec comme contenu un stream ou un bytes array.
Si vous utilisez ces méthodes aucun sérialiseur ne sera utilisé.

Streams

Lire une réponse de type Stream :

// Read stream response
 Stream stream = await client.
              GetRequest("File").
              ExecuteAsStreamAsync();

Envoyer un contenu du type Stream :

// Post Stream as content
await client.PostRequest("File/Add").
            AddStreamContent(stream).
            ExecuteAsync();

byte[]

Lire une réponse de type byte[] :

// Read byte array response         
byte[] byteArray = await client.
              GetRequest("File").
              ExecuteAsByteArrayAsync();

<em><strong>Envoyer un contenu du type byte[] :</strong></em>

// Send bytes array as content
await client.
            PostRequest("File/Add").
            AddByteArrayContent(byteArray).
            ExecuteAsync();

Gestion des erreurs :

Les requêtes peuvent lancer 4 types d'exceptions :
* ConnectionException : Lancée quand la requête ne peut pas atteindre le serveur
* HttpException : Lancée quand la requête a atteint le serveur mais que le StatusCode est invalide (404, 500...)
* SerializeException : Lancée quand le sérialiseur ne peut sérialiser le contenu
* DeserializeException : Lancée quand le déserialiseur ne peut déserialiser la réponse
* TimeoutException : Lancée lorsque la requête prend trop de temps a s’exécuter

Rattraper un Status code spécifique

string cityName = "Paris";
try
{ 
   var response = await client.
     GetRequest("City").
     AddQueryParameter("Name", cityName).
     ExecuteAsync<City>();
}
catch (HttpException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
   throw new CityNotFoundException(cityName);
}
catch (HttpException ex) when (ex.StatusCode == System.Net.HttpStatusCode.InternalServerError)
{
   throw new ServerErrorException($"{ex.Message} {ex.ReasonPhrase}");
}

Formatters

Par défaut :
- Le formateur Json est utilisé par défaut.
- Un formateur Xml est ajouté dans la liste des formateurs supportés

Chaque formateur a une liste de "Supported Media Types". Cela permet au RestClient de détecter quel formateur sera utilisé.
Si aucun formateur ne correspond, il utilisera le formateur par défaut.

Ajouter un nouveau formateur

Ajouter un formateur comme formateur par défaut.

bool isDefaultFormatter = true;
var customFormatter = new CustomFormatter();
client.Settings.Formatters.Add(customFormatter, isDefaultFormatter);

Supprimer un formateur :

var lastFormatter = client.Settings.Formatters.Where( f=> f is XmlSerializer>).First();
client.Remove(lastFormatter);

Définir un sérialiseur pour la requête courante :

IFormatter serializer = new XmlFormatter();
 var response = await client.
     PostRequest("City", city, serializer).
     ExecuteAsync();

Définir un désérialiseur pour la requête courante :

IFormatter deserializer = new XmlFormatter();

 var response = await client.
     GetRequest("City").
     AddQueryParameter("Name", cityName).
     ExecuteAsync<City>(deserializer);

Formateur personnalisé :

Vous pouvez créer votre propre formateur en implémentant l'interface IFormatter.

Voici par exemple l'implémentation du XMLFormatter :

public class XmlFormatter : IFormatter
{

   public string DefaultMediaType => "application/xml";

   public IEnumerable<string> SupportedMediaTypes
   {
      get
      {
         yield return "application/xml";
         yield return "text/xml";
      }
   }

   public T Deserialize<T>(Stream stream, Encoding encoding)
   {
      using (var reader = new StreamReader(stream, encoding))
      {
         var serializer = new XmlSerializer(typeof(T));
         return (T)serializer.Deserialize(reader);
      }
   }

   public string Serialize<T>(T data, Encoding encoding)
   {
         if (data == default)
         {
             return null;
         }

         var serializer = new XmlSerializer(data.GetType());
         using (var stringWriter = new DynamicEncodingStringWriter(encoding))
         {
            serializer.Serialize(stringWriter, data);
            return stringWriter.ToString();
         }
      }
   }

Listener

Vous pouvez ajouter simplement un IListener pour "écouter" toutes les requêtes / réponses / exceptions reçues.

Debug Listener

Un listener Debug est fourni avec la librairie.
Pour l'ajouter, il suffit d'appeler la méthode AddDebug sur la propriété Listener.

client.Settings.Listeners.AddDebug();

Vous pouvez aussi créer votre propre listener en implémentant l'interface IListener.

Postman Listener

Pour ajouter un listener Postman vous avez a appeler la méthode AddPostman sur la propriété Listeners

PostManListerner listener = client.Settings.Listeners.AddPostman("nameOfCollection");

Vous pouvez sauvegarder la collection Postman en appelant SaveAsync du listener Postman.

await listener.SaveAsync(new FileInfo("postManCollection.json");

Si vous voulez uniquement le Json de la collection, vous pouvez appeler la méthode GetCollectionJson

await listener.GetCollectionJson();

Ajout d'un listener personnalisé

IListener myCustomListerner = ..
client.Settings.Listeners.Add(myCustomListerner);

Pour aller plus loin :

- La page Github.
- La page nuget.
- Le chat Gitter

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Pin It on Pinterest