[![CI](https://github.com/jgiacomini/Tiny.RestClient/actions/workflows/ci.yml/badge.svg)](https://github.com/jgiacomini/Tiny.RestClient/actions/workflows/ci.yml) [![NuGet](https://img.shields.io/nuget/v/Tiny.RestClient.svg?label=NuGet)](https://www.nuget.org/packages/Tiny.RestClient/) [![StackOverflow](https://img.shields.io/badge/questions-on%20StackOverflow-orange.svg?style=flat)](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); ```