FluentHttpClient is a modern async HTTP client for REST APIs. Its fluent interface lets you send an HTTP request and parse the response in one go — hiding away the gritty details like deserialisation, content negotiation, optional retry logic, and URL encoding:
Blog result = await new FluentClient("https://example.org/api")
.GetAsync("blogs")
.WithArgument("id", 15)
.WithBearerAuthentication(token)
.As<Blog>();
Designed with discoverability and extensibility as core principles, just autocomplete to see which methods are available at each step.
Install it from NuGet:
Install-Package Pathoschild.Http.FluentClient
The client works on any modern platform (including Linux, Mac, and Windows):
platform | min version |
---|---|
.NET Framework | 4.5 |
.NET Core | 1.0 |
.NET Standard | 1.3 |
Universal Windows Platform | 10 |
You start by creating a client for an API. You can use this client for one request, or reuse it for many requests for improved performance using its built-in connection pool.
IClient client = new FluentClient("https://example.org/api/");
Then you just chain methods to set up the request and get the response. For example, here's a GET
request which returns an Item
model (automatically deserialised based on content negotiation):
Item item = await client
.GetAsync("items/14")
.As<Item>();
You can get the response as a model, array of models, bytes, string, or stream:
string content = await client
.GetAsync("items/14")
.AsString();
If you don't need the response content, you can just wait for the request to complete.
await client.PostAsync("items", new Item(…));
You can quickly configure complex requests with the fluent methods. Here's a more complicated example:
Message[] messages = await client
.GetAsync("messages/latest")
.WithHeader("Content-Type", "application/json")
.WithArguments(new { id = 14, tenant = "acme" })
.WithBearerAuthentication(token)
.AsArray<Message>();
The above examples all parse the response directly, but sometimes you want to peek at the HTTP metadata:
IResponse response = await client.GetAsync("messages/latest");
if (response.Status == HttpStatusCode.OK)
return response.AsArray<T>();
By default the client will throw ApiException
if the server returns an error code:
try
{
await client.Get("items");
}
catch(ApiException ex)
{
string responseText = await ex.Response.AsString();
throw new Exception($"The API responded with HTTP {ex.Response.Status}: {responseText}");
}
If you don't want that, you can...
-
disable it for one request:
IResponse response = await client .GetAsync("items") .WithOptions(ignoreHttpErrors: true);
-
disable it for all requests:
client.SetOptions(ignoreHttpErrors: true);
The client supports JSON and XML out of the box. If you need more, you can...
-
Add any of the existing media type formatters:
client.Formatters.Add(new YamlFormatter());
-
Create your own by subclassing
MediaTypeFormatter
(optionally using the includedMediaTypeFormatterBase
class).
The client won't retry failed requests by default, but that's easy to configure:
client
.SetRequestCoordinator(
maxRetries: 3,
shouldRetry: request => request.StatusCode != HttpStatusCode.OK,
getDelay: (attempt, response) => return TimeSpan.FromSeconds(attempt), // 1, 2, and 3 seconds
retryOnTimeout: true
);
If that's not enough, implementing IRequestCoordinator
lets you control how the client
dispatches requests. (You can only have one request coordinator on the client; you should use
HTTP filters instead for most overrides.)
For example, here's a simple retry coordinator using Polly:
/// <summary>A request coordinator which retries failed requests with a delay between each attempt.</summary>
public class RetryCoordinator : IRequestCoordinator
{
/// <summary>Dispatch an HTTP request.</summary>
/// <param name="request">The response message to validate.</param>
/// <param name="send">Dispatcher that executes the request.</param>
/// <returns>The final HTTP response.</returns>
public async Task<HttpResponseMessage> ExecuteAsync(IRequest request, Func<IRequest, Task<HttpResponseMessage>> send)
{
int[] retryCodes = { 408, 500, 502, 503, 504 };
return Policy
.HandleResult<HttpResponseMessage>(request => retryCodes.Contains((int)request.StatusCode))
.Retry(3, async () => await send(request));
}
}
...and here's how you'd set it:
client.SetRequestCoordinator(new RetryCoordinator());
You can read and change the underlying HTTP requests and responses by creating IHttpFilter
implementations. They can be useful for automating custom authentication or error-handling.
For example, the default error-handling is just a filter:
/// <summary>Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response.</summary>
/// <param name="response">The HTTP response.</param>
/// <param name="httpErrorAsException">Whether HTTP error responses (e.g. HTTP 404) should be raised as exceptions.</param>
public void OnResponse(IResponse response, bool httpErrorAsException)
{
if (httpErrorAsException && !response.Message.IsSuccessStatusCode)
throw new ApiException(response, $"The API query failed with status code {response.Message.StatusCode}: {response.Message.ReasonPhrase}");
}
...which you can replace with your own:
client.Filters.Remove<DefaultErrorFilter>();
client.Filters.Add(new YourErrorFilter());
You can do much more with HTTP filters by editing the requests before they're sent or the responses before they're parsed:
/// <summary>Method invoked just before the HTTP request is submitted. This method can modify the outgoing HTTP request.</summary>
/// <param name="request">The HTTP request.</param>
public void OnRequest(IRequest request)
{
// example only — you'd normally use a method like client.SetAuthentication(…) instead.
request.Message.Headers.Authorization = new AuthenticationHeaderValue("token", "…");
}
For advanced scenarios, you can customise the underlying HttpClient and HttpClientHandler. For example, here's how to create mock requests for unit testing using RichardSzalay.MockHttp:
// create mock
var mockHandler = new MockHttpMessageHandler();
mockHandler.When(HttpMethod.Get, "https://example.org/api/items").Respond(HttpStatusCode.OK, testRequest => new StringContent("[]"));
// create client
var client = new FluentClient("https://example.org/api", new HttpClient(mockHandler));
The client fully supports .NET cancellation tokens if you need to abort requests:
var tokenSource = new CancellationTokenSource();
await client
.PostAsync(…)
.WithCancellationToken(tokenSource.Token);
tokenSource.Cancel();
The client is built around the async
and await
keywords, but you can use the client
synchronously. That's not recommended — it complicates error-handling (e.g. errors get wrapped
into AggregateException), and it's very easy to cause thread deadlocks when you do this (see
Parallel Programming with .NET: Await, and UI, and deadlocks! Oh my! and
Don't Block on Async Code).
If you really need to use it synchronously, you can just call the Result
property:
Item item = client.GetAsync("items/14").Result;
Or if you don't need the response:
client.PostAsync("items", new Item(…)).AsResponse().Wait();