-
Notifications
You must be signed in to change notification settings - Fork 1
Version 3.x
FluentHttpClient exposes a set of extensions methods to make sending REST requests with HttpClient
both readable and chainable.
FluentHttpClient is available on NuGet.org and can be installed using a NuGet package manager or the .NET CLI.
The socket exhaustion problems associated with the incorrect usage of the HttpClient
class in .NET applications has been well documented. Microsoft has published an article introducing IHttpClientFactory
, which is used to configure and create HttpClient
instances in an app.
- You're Using HttpClient Wrong And It Is Destabilizing Your Software
- Make HTTP requests using IHttpClientFactory in ASP.NET Core
- Use IHttpClientFactory to implement resilient HTTP requests
Using HttpClient
involves creating an HttpRequestMessage
, configuring it's properties (e.g. headers, query string, route, etc.), serializing the content, and sending that request. The response then needs to be deserialized and used.
The extension methods available in this library simplify that lifecycle. The UsingRoute
extension method on HttpClient
returns a HttpRequestBuilder
object, which has extension methods on it to configure the request. It also has extension methods to send the request using different HTTP verbs, and then there are extension methods on both HttpResponseMessage
and Task<HttpResponseMessage>
for deserializing the content. Put another way, the extension methods fall in to three categories.
- Configuring the
HttpRequestMessage
- Sending the
HttpRequestMessage
- Deserializing the
HttpResponseMessage
contents
To enjoy the benefits of using these chaining methods, you can configure the request and send it all in one chain. The example below proceeds in that topical order.
var content = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync()
.GetResponseStreamAsync();
Note that the method name difference between setting a single instance of a property and multiples is that the multiple instance will use the plural form. For example, you can add a single query parameter using the method
WithQueryParam()
and multiple query parameters at once usingWithQueryParams()
.
Start by setting the request route using the UsingRoute(string route)
extension method. If the HttpClient.BaseAddress
has already been set, the value should be relative to that value (don't worry about striping or adding leading slashes, the library will take care of that as needed). If it has not, then you should include the fully qualified domain name and full path the endpoint.
_client.UsingRoute("/repos/scottoffen/grapevine/issues");
You'll notice that this is the only extension method on HttpClient
of those listed here. This method actually returns an instance of HttpRequestBuilder
, and all other request configuration methods below are extension methods on that class.
If your requests need authentication, you can easily add it using one of the three extensions methods below.
This is not necessary if you have already configured authorization tokens on your client.
Basic authentication sends a Base64 encoded username and password. You can create the Base64 encoded string yourself or let the extension method do that for you.
// Send the username and password to be concatenated and Base64 encoded
_client.UsingRoute("some/route/here")
.WithBasicAuthentication("username", "password");
// Concat and encode yourself, and just pass in the token
var token = "dXNlcm5hbWU6cGFzc3dvcmQ=";
_client.UsingRoute("some/route/here")
.WithBasicAuthentication(token);
_client.UsingRoute("some/route/here")
.WithOAuthBearerToken(bearerToken);
Or you can use a different authentication scheme by passing in the type and credentials.
_client.UsingRoute("some/route/here")
.WithAuthentication("type", "credentials");
You can set the content of the request by passing in a string or a pre-built HttpContent
object.
// Send a string of data
var json = JsonSerializer.Serialize(myObject);
_client.UsingRoute("some/route/here")
.WithContent(myStringData);
// Send a multipart request
MultipartContent content = ...
_client.UsingRoute("some/route/here")
.WithContent(content);
// Or more specifically multipart/form-data
MultipartFormDataContent content = ...
_client.UsingRoute("some/route/here")
.WithContent(content);
If you are using .NET 5.0 or higher, you can simplify sending objects as JSON using the .WithJsonContent()
method.
var obj = new SomeDtoClass();
_clientUsingRoute("some/route/here")
.WithJsonContent(obj);
With this method you can send an optional instance of JsonSerializerOptions
to modify how the object is serialized.
You can set one or many cookies on the request.
// Set a single cookie
_client.UsingRoute("some/route/here")
.WithCookie("key", "value");
// Set multiple cookies
var cookies = new []
{
new KeyValuePair<string,string>("name", "John Smith"),
new KeyValuePair<string,string>("delta", "true")
};
_client.UsingRoute("some/route/here")
.WithCookies(cookies);
You can set one or many headers on the request.
// Set a single header
_client.UsingRoute("some/route/here")
.WithHeader("X-CustomHeader", "MyCustomValue");
// Set multiple headers
var headers = new []
{
new KeyValuePair<string,string>("Content-Type", "application/json"),
new KeyValuePair<string,string>("Content-Encoding", "gzip")
};
_client.UsingRoute("some/route/here")
.WithHeaders(headers);
Add query parameters to the route.
// Set a single query parameter
_client.UsingRoute("some/route/here")
.WithQueryParam("id", "13485");
// Set multiple query parameters
var query = new NameValueCollection()
{
{"state", "open"},
{"sort", "created"},
{"direction", "desc"},
};
_client.UsingRoute("some/route/here")
.WithQueryParams(query);
You can set the timeout value for a request by passing either a number of seconds as an integer or passing in a TimeSpan
instance.
// Set to 10 seconds
_client.UsingRoute("some/route/here")
.WithRequestTimeout(10);
// Set to a timespan of 10 seconds
_client.UsingRoute("some/route/here")
.WithRequestTimeout(TimeSpan.FromSeconds(10));
You can provide an exception handler for when an HttpRequestException
is thrown. When used, success and failure handlers will not be executed, and the GetResponse*
methods will return 0
. If you are using any deserialization methods, you will want to provided a default action to avoid an exception being thrown when the deserialization attempt is made.
var response = await _clientUsingRoute("some/route/here")
.OnHttpRequestException(ex =>
{
/* The exception will be available in this context */
})
.GetAsync()
.OnSuccessAsync(async msg => { /* code will not execute if exception is thrown */ })
.OnFailureAsync(async msg => { /* code will not execute if exception is thrown */ })
.GetResponseStringAsync() // Will return '0' if HttpRequestException occurred
var response = await _clientUsingRoute("some/route/here")
.OnHttpRequestException(ex =>
{
/* The exception will be available in this context */
})
.GetAsync()
.OnSuccessAsync(async msg => { /* code will not execute if exception is thrown */ })
.OnFailureAsync(async msg => { /* code will not execute if exception is thrown */ })
.DeserializeJsonAsync<SomeClass>(async msg =>
{
/* This code will execute INSTEAD OF the deserialization if an HttpRequestException occurred */
});
There are extension methods specifically for the most common HTTP methods, or you can pass in the method to be used. All of the methods take an optional cancellation token (not shown).
var response = await _client
.UsingRoute("/user/list")
.WithQueryParam("sort", "desc")
.GetAsync();
var response = await _client
.UsingRoute("/user")
.WithContent(userdata)
.PostAsync();
var response = await _client
.UsingRoute("/user/1234")
.WithContent(userdata)
.PutAsync();
var response = await _client
.UsingRoute("/user/234")
.DeleteAsync();
var response = await _client
.UsingRoute("/user")
.WithContent(userdata)
.SendAsync(HttpMethod.Patch);
Each of the methods GetAsync()
, PostAsync()
, PutAsync()
and DeleteAsync()
call SendAsync()
behind the scenes. As such, GetAsync()
is equivalent to SendAsync(HttpMethod.Get)
.
These are extensions methods on
Task<HttpResponseMessage>
, and are only available to be chained after the request has been sent.
There are times when the body of the response might not contain any data - or you just might not care about the data - and therefore there is no need to deserialize the response. You can add callback delegates for success and failure using the following methods:
OnSuccess(msg => { /* code to execute */ })
OnSuccessAsync(async msg => { /* async code to execute */ })
OnFailure(msg => { /* code to execute */ })
OnFailureAsync(async msg => { /* async code to execute */ })
bool success = false;
var response = await _client
.UsingRoute("/user/list")
.WithQueryParam("sort", "desc")
.GetAsync()
.OnFailure(msg => { success = false; })
.OnSuccess(msg => { success = true; });
Use the async versions when you need to perform awaitable tasks in your callback (e.g. parsing the response body). The single parameter to the delegate is of type HttpResponseMessage
.
The extensions methods for failure take a second, optional parameter after the delegate that indicates whether or not you want an exception to be thrown if the status code does not indicate success. This parameter is named suppressException
. If it is false, then the EnsureSuccessStatusCode()
method is called on the HttpResponseMessage after your delegate is run.
In version 1.x the
suppressException
parameter is false by default, and in all future versions it is true by default.
You can deserialize the response body to a string, stream or byte array using an extension method on the response.
string response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync()
.GetResponseStringAsync();
/* OR */
var response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync();
var responseContent = await response.GetResponseStringAsync();
string response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync()
.GetResponseStreamAsync();
/* OR */
var response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync();
var responseContent = await response.GetResponseStreamAsync();
string response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync()
.GetResponseBytesAsync();
/* OR */
var response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync();
var responseContent = await response.GetResponseBytesAsync();
Deserialize a JSON response to an object from either a string or a stream.
string response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync()
.GetResponseStreamAsync()
.DeserializeJsonAsync<IssuesResponse>();
An optional JsonSerializerOptions
instance can be passed as a parameter.
If the request does not return a success status code, it might not be possible to deserialize the response body to the desired object. In those cases, you can specify a default action that will occur instead of the default deserialization if the status code on the response is not a success status code.
string response = await _client
.UsingRoute("/repos/scottoffen/grapevine/issues")
.WithQueryParam("state", "open")
.WithQueryParam("sort", "created")
.WithQueryParam("direction", "desc")
.GetAsync()
.GetResponseStreamAsync()
.DeserializeJsonAsync<IssuesResponse>(msg =>
{
/*
* write to the logs, throw a custom exception or parse the problem details
* take different actions based on the status code
* or return a default empty object
*/
return new IssueResponse();
});
If you need to perform async tasks in your delegate, preface it with the async
keyword.
.DeserializeJsonAsync<IssuesResponse>(async msg =>
{
/* do async stuff here */
});
Create custom fluent deserializers by adding generic async extensions on Task<Stream>
and/or Task<string>
. For a given generic format (e.g. Xml):
public static class FluentHttpClientExtensions
{
public static async Task<T?> DeserializeFormatAsync<T>(this Task<Stream> result)
{
return await FormatSerializer.DeserializeFormatAsync<T>(await result);
}
}
Unless explicitly passed in the method call for serialization or deserialization of JSON, the following default JsonSerializerOptions
will be used:
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
This default can be overridden or modified via the FluentHttpClientOptions.DefaultJsonSerializerOptions
property.