[](https://github.com/jgiacomini/Tiny.RestClient/actions/workflows/ci.yml)
[](https://www.nuget.org/packages/Tiny.RestClient/)
[](http://stackoverflow.com/questions/tagged/tiny.restclient)
[Please visit the main site.](https://jgiacomini.github.io/Tiny.RestClient/)
Tiny.RestClient facilitates the dialog between your API and your application.
It hides all the complexity of communication, deserialisation ...
## Platform Support
Built for **.NET Standard 2.0**, **.NET Standard 2.1**, **.NET 8.0** and **.NET 10.0**, which allows you to use it with :
- .NET Standard 2.0 / 2.1 compatible runtimes
- .NET 8.0 (LTS)
- .NET 10.0 (LTS) and superior (Blazor, ASP.NET Core, ...)
## Features
* Modern async http client for REST API.
* Support of verbs : GET, POST , PUT, DELETE, PATCH and custom http verbs
* Support of ETag
* Support of multi-part form data
* Support of cancellation token on each requests
* Support of : download file and Upload file
* Support of Server-Sent Events (SSE) streaming (on .NET Standard 2.1, .NET 8.0 and .NET 10.0)
* Automatic XML and JSON serialization / deserialization
* Support of custom serialisation / deserialisation
* Support of camelCase, snakeCase kebabCase for json serialization
* Typed exceptions which are easier to interpret
* Define timeout globally or per request
* Timeout exception thrown if the request is in timeout (by default HttpClient sends OperationCancelledException, so we can't distinguish between user cancellation and timeout)
* Provide an easy way to log : all sending of request, failed to get response, and the time get response.
* Support of export requests to postman collection
* Support of display cURL requests in debug output
* Support of Basic Authentification
* Support of OAuth2 Authentification
## Basic usage
### Create the client
```cs
using Tiny.RestClient;
var client = new TinyRestClient(new HttpClient(), "http://MyAPI.com/api");
```
### Headers
#### Default header for all requests
```cs
// Add default header for each calls
client.Settings.DefaultHeaders.Add("CustomHeader", "Header");
```
```cs
// Add Auth2.0 token
client.Settings.DefaultHeaders.AddBearer("token");
```
```cs
// Add default basic authentication header
client.Settings.DefaultHeaders.AddBasicAuthentication("username", "password");
```
#### Add header for current request
```cs
// Add header for this request only
client.GetRequest("City/All").
AddHeader("CustomHeader", "Header").
ExecuteAsync();
```
```cs
// Add header for this request only
client.GetRequest("City/All").
WithOAuthBearer("MYTOKEN").
ExecuteAsync();
```
```cs
// Add basic authentication for this request only
client.GetRequest("City/All").
WithBasicAuthentication("username", "password").
ExecuteAsync();
```
#### Calculate headers before send the requests
Before send requests to server we can add calculate dynamically the headers to add to resquest like below :
```cs
client.Settings.CalculateHeadersHandler = async () =>
{
var token = await GetACustomTokenAsync();
var headers = new Headers
{
{ "CustomToken", token },
};
return headers;
};
```
#### Read headers of response
```cs
await client.GetRequest("City/GetAll").
FillResponseHeaders(out headersOfResponse Headers).
ExecuteAsync();
foreach(var header in headersOfResponse)
{
Debug.WriteLine($"{current.Key}");
foreach (var item in current.Value)
{
Debug.WriteLine(item);
}
}
```
### Basic GET http requests
```cs
var cities = client.GetRequest("City/All").ExecuteAsync>();
// 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> ();
// GET http://MyAPI.com/api/City?id=2&country=France deserialize automaticaly the content
```
### Basic POST http requests
```cs
// POST
var city = new City() { Name = "Paris" , Country = "France"};
// With content
var response = await client.PostRequest("City", city).
ExecuteAsync();
// 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();
// 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();
// POST text file at http://MyAPI.com/api/City/Add
```
### Custom Http Verb requests
```cs
await client.
NewRequest(new System.Net.Http.HttpMethod("HEAD"), "City/All").
ExecuteAsync>();
```
### Define timeout
Define a global timeout for all client. (By default it's setted to 100 secondes)
```cs
client.Settings.DefaultTimeout = TimeSpan.FromSeconds(100);
```
Define the timeout for one request
```cs
request.WithTimeout(TimeSpan.FromSeconds(100));
```
### Allow non http 2xx responses
#### Globaly
Allow any status codes :
```cs
client.Settings.HttpStatusCodeAllowed.AllowAnyStatus = true;
```
Allow only a range of http status codes :
```cs
client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(400, 420));
```
or
```cs
client.Settings.HttpStatusCodeAllowed.Add(new HttpStatusRange(System.Net.HttpStatusCode.BadRequest, System.Net.HttpStatusCode.BadGateway));
```
#### By request
Allow all status code :
```cs
request.AllowAllHttpStatusCode().ExecuteAsync();
```
Allow only a range of http status codes :
```cs
request.AllowRangeHttpStatusCode(400, 420).ExecuteAsync();
```
Allow only on stats code of http status codes :
```cs
request.AllowSpecificHttpStatusCode(409).ExecuteAsync();
```
### Download file
```cs
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
```
## Get raw HttpResponseMessage
```cs
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
```
## Get raw string result
```cs
string response = await client.
GetRequest("City/All").
ExecuteAsStringAsync();
// GET http://MyAPI.com/api/City/All with from url encoded content
```
## Server-Sent Events (SSE)
> Available on **.NET Standard 2.1**, **.NET 8.0** and **.NET 10.0** only.
> This feature relies on `IAsyncEnumerable` and is not available on .NET Standard 2.0.
`ExecuteAsSSEAsync` opens a streaming connection and yields each event as it is received.
The connection stays open until the server closes the stream or the `CancellationToken` is cancelled.
Unlike the other `ExecuteAs...` methods, the response body is **not** buffered : events are delivered as soon as they arrive.
```cs
await foreach (var sse in client.
GetRequest("notifications/stream").
ExecuteAsSSEAsync(cancellationToken))
{
Console.WriteLine($"id : {sse.Id}");
Console.WriteLine($"event : {sse.Event}");
Console.WriteLine($"data : {sse.Data}");
Console.WriteLine($"retry : {sse.Retry}");
}
// GET http://MyAPI.com/api/notifications/stream and stream each event
```
Each `ServerSentEvent` exposes the standard SSE fields :
| Property | SSE field | Description |
| -------- | --------- | ----------- |
| `Data` | `data` | The event payload. Multiple `data:` lines are joined with a line feed (`\n`). |
| `Event` | `event` | The event type. Defaults to `message` when the server does not provide one. |
| `Id` | `id` | The event identifier, or `null` if not provided. |
| `Retry` | `retry` | The reconnection time in milliseconds, or `null` if not provided. |
Comment lines (starting with `:`) are ignored, as are unknown fields, per the SSE specification.
To stop listening, cancel the `CancellationToken` you passed to `ExecuteAsSSEAsync` :
```cs
using var cts = new CancellationTokenSource();
// Stop the stream after 30 seconds
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
await foreach (var sse in client.GetRequest("events").ExecuteAsSSEAsync(cts.Token))
{
Process(sse);
}
}
catch (OperationCanceledException)
{
// Stream stopped
}
```
## Multi-part form data
```cs
// 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(city1, "city1", "city1.json").
AddContent(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();
// With 2 strings content
var response = await client.
PostRequest("City/Image/Text").
AsMultiPartFromDataRequest().
AddString("string1", "text/plain").
AddString("string2", "text/plain").
ExecuteAsync();
// With mixed content
await client.PostRequest("Files/Add").
AsMultiPartFromDataRequest().
AddContent(city1, "city1", "city1.json").
AddByteArray(byteArray1, "request", "request2.bin").
AddStream(stream2, "request", "request2.bin")
AddString("string1", "text", "request.txt")
ExecuteAsync();
```
## String, Streams and bytes array
You can use as content : strings, streams or byte arrays.
If you use these methods no serializer will be used.
### String
```cs
// Read string response
Stream stream = await client.
GetRequest("text").
ExecuteAsStringAsync();
// Post String as content
await client.PostRequest("poetry/text").
AddStringContent(stream).
ExecuteAsync();
```
### Streams
```cs
// Read stream response
Stream stream = await client.
GetRequest("File").
ExecuteAsStreamAsync();
// Post Stream as content
await client.PostRequest("File/Add").
AddStreamContent(stream).
ExecuteAsync();
```
### Byte array
```cs
// Read byte array response
byte[] byteArray = await client.
GetRequest("File").
ExecuteAsByteArrayAsync();
// Read byte array as content
await client.
PostRequest("File/Add").
AddByteArrayContent(byteArray).
ExecuteAsync();
```
## Error handling
All requests can throw 5 exceptions :
* ConnectionException : thrown when the request can't reach the server
* HttpException : thrown when the server has invalid error code
* SerializeException : thrown when the serializer can't serialize the content
* DeserializeException : thrown when the deserializer can't deserialize the response
* TimeoutException : thrown when the request take too much time to be executed
### Catch a specific error code
```cs
string cityName = "Paris";
try
{
var response = await client.
GetRequest("City").
AddQueryParameter("Name", cityName).
ExecuteAsync();
}
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}");
}
```
### Enclapsulate HttpExceptions
We can setup a global handler to provide a logic to encapsulate HttpException automatically.
For example I can choose to translate all HttpException with StatusCode NotFound in a NotFoundCustomException.
```cs
client.Settings.EncapsulateHttpExceptionHandler = (ex) =>
{
if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new NotFoundCustomException();
}
return ex;
};
```
Now if I call an API wich respond with status code NotFound it will throw automaticaly my custom exception.
```cs
// Call an API wich throw NotFound error
await client.GetRequest("APIWhichNotExists").ExecuteAsync();
```
## ETag
The lib supports the Entity tag but it's not enabled by default.
### Define an ETagContainer globally
An implementation of IETagContainer is provided. It stores all data in multiples files.
To enable it :
```cs
client.Settings.ETagContainer = new ETagFileContainer(@"C:\ETagFolder");
```
### Define an ETagContainer for one request
You can also define the ETagContainer only on specific request.
```cs
request.WithETagContainer(eTagContainer);
```
## Formatters
By default :
* the Json is used as default Formatter.
* Xml Formatter is added in Formatters
Each formatter has a list of supported media types.
It allows TinyRestClient to detect which formatter will be used.
If no formatter is found it uses the default formatter.
### Add a new formatter
Add a new custom formatter as default formatter.
```cs
bool isDefaultFormatter = true;
var customFormatter = new CustomFormatter();
client.Settings.Formatters.Add(customFormatter, isDefaultFormatter);
```
### Remove a formatter
```cs
var lastFormatter = client.Settings.Formatters.First(f => f is XmlSerializer);
client.Settings.Formatters.Remove(lastFormatter);
```
### Json custom formatting
You can enable 4 types of formatting on JsonFormatter :
- PascalCase (PropertyName => PropertyName)
- CamelCase (PropertyName => propertyName) (default)
- SnakeCase (PropertyName => property_name)
- KebabCase (aslo known as SpinalCase) (PropertyName => property-name).
```cs
// Enable CamelCase
client.Settings.Formatters.OfType().First().UseCamelCase();
```
By default the JSONFormatter use camelCase.
```cs
// Enable PascalCase
client.Settings.Formatters.OfType().First().UsePascalCase();
```
```cs
// Enable KebabCase
client.Settings.Formatters.OfType().First().UseKebabCase();
```
```cs
// Enable SnakeCase
client.Settings.Formatters.OfType().First().UseSkakeCase();
```
### Define a specific serialize for one request
```cs
IFormatter serializer = new XmlFormatter();
var response = await client.
PostRequest("City", city, serializer).
ExecuteAsync();
```
### Define a specific deserializer for one request
```cs
IFormatter deserializer = new XmlFormatter();
var response = await client.
GetRequest("City").
AddQueryParameter("Name", cityName).
ExecuteAsync(deserializer);
```
### Custom formatter
You can create your own serializer/deserializer by implementing IFormatter
For example the implementation of XmlFormatter is really simple :
```cs
public class XmlFormatter : IFormatter
{
public string DefaultMediaType => "application/xml";
public IEnumerable SupportedMediaTypes
{
get
{
yield return "application/xml";
yield return "text/xml";
}
}
public ValueTask DeserializeAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken)
{
using (var reader = new StreamReader(stream, encoding))
{
var serializer = new XmlSerializer(typeof(T));
return ValueTask.FromResult((T)serializer.Deserialize(reader));
}
}
public Task SerializeAsync(T data, Encoding encoding, CancellationToken cancellationToken)
{
if (data == default)
{
return null;
}
var serializer = new XmlSerializer(data.GetType());
using (var stringWriter = new DynamicEncodingStringWriter(encoding))
{
serializer.Serialize(stringWriter, data);
return Task.FromResult(stringWriter.ToString());
}
}
}
```
## Listeners
You can easily add a listener to listen all the sent requests / responses received and all exceptions.
Two listeners are provided by the lib :
* A debug listener : which logs all requests in debug console
* A postman listener : which allows you to export all your requests as a postman collection
### Debug Listener
To add a Debug listener you have to call AddDebug on Listeners property
```cs
client.Settings.Listeners.AddDebug();
```
### cURL Listener
To add a cURL listener you have to call AddCurl on Listeners property
```cs
client.Settings.Listeners.AddCurl();
```
It produce this type of output in debug window for each ExecuteAsync called :
```cs
curl -X POST "http://localhost:4242/api/PostTest/complex"-H "Accept: application/json" -H "Content-Type: application/json" -d "{\"Id\":42,\"Data\":\"DATA\"}"
```
### Postman Listener
To add a postman listener you have to call AddPostman on Listeners property
```cs
PostmanListerner postmanListener = client.Settings.Listeners.AddPostman("nameOfCollection");
```
When you want to save the postman collection you have to call SaveAsync
```cs
await postmanListener.SaveAsync(new FileInfo("postmanCollection.json");
```
If you only want the Json of collection you can call the method GetCollectionJson
```cs
listener.GetCollectionJson();
```
### Custom Listener
You can also create you own listener by implementing IListener.
```cs
IListener myCustomListerner = ..
client.Settings.Listeners.Add(myCustomListerner);
```