I'm very happy to present a project I've been working on for a while: a fully fluid .NetStandard compatible REST client.

The code is available on GitHub.

And the software library is on Noise.

You want to talk about it? Gitter

The main client objective is to have an optimized and easy to use REST client.

It is compatible with .NET Standard 1.3 and 2.0 which means that it can be used on .NET Framework 4.5 or higher, Xamarin and .NET Core and UWP.

Functionalities:

* A modern client for making asynchronous calls for REST APIs

* Support for the main HTTP verbs: GET, POST, PUT, DELETE, PATCH

* Support for personalized verbs

* Support for cancellation token on all types of requests

* Automatic detection of the deserializer to be used

* Support for serialization/deserialization of JSON and XML

* Customized serial/deserial support

* Support for multi-part form (sending multiple files)

* Optimized HTTP calls

* Easier to interpret typed exceptions

* Provides a simple way to log all queries: the answers that fail, the response time...

* Possibility to set the timeout overall or by request

* LAPIAPI launches a Timeout exception when requests are in timeout (default HttpClient API launches an OperationCancelledException exception which makes it impossible to differentiate between a token cancellation and a timeout)

* Allows to export requests as a Postman collection

Creating a customer


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

Headers

Add a default header for all requests


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

Add a header for the current request


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

Read the answer headers


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);
    }
}

Creating a GET query


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

Creating a POST query


// 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 

Create a query with a custom HTTP verb


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

Defining the timeout

Defining a global timeout


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

Set the timeout for a request


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

Downloading a file


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 

How to recover an HttpResponse message


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

Read an answer as String


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

Making multi-part requests

Creating multiparty queries is very simple with the 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 and byte[]

You can make queries with content like a stream or bytes array.

If you use these methods, no serialisers will be used.

Streams

Read a stream-like response:


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

Send content of the Stream type:


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

byte[]

Read a byte response[]:


// 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();

Error management:

Applications may introduce four types of exemptions:

* ConnectionException: Launched when the request cannot reach the server

* HttpException: Launched when the request reached the server but the StatusCode is invalid (404, 500...)

* SerializeException: Launched when the serializer cannot serialize content

* DeserializeException: Launched when the deserializer cannot deserialize the response

* TimeoutException: Launched when the request takes too long to execute

Catch up with a specific Status code


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}");
}

Trainer

By default:

  • The Json trainer is used by default.

  • An Xml trainer is added to the list of supported trainers

Each trainer has a list of "Supported Media Types". This allows the RestClient to detect which trainer will be used.

If no trainer matches, he will use the default trainer.

Adding a new trainer

Add a trainer as the default trainer.


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

Removing a trainer:


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

Define a serial for the current request:


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

Define a deserializer for the current request:


IFormatter deserializer = new XmlFormatter();

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

Personalised trainer:

You can create your own trainer by implementing the IFormatter interface.

For example, the XMLFormatter implementation is as follows:


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

You can simply add an IListener to "listen" to any queries/answers/exceptions received.

Debug listener

A Debug listener comes with the software library.

To add it, just call the AddDebug method on the property Listener.


client.Settings.Listeners.AddDebug();

You can also create your own listener by implementing the IListener interface.

Postman listener

To add a Postman listener you have to call the AddPostman method on the Listeners property


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

You can save the Postman collection by calling SaveAsync from the Postman listener.


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

If you only want the Json of the collection, you can call the GetCollectionJson method


await listener.GetCollectionJson();

Adding a custom listener


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

To go further: