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