Code Monkey home page Code Monkey logo

fluenthttpclient's Introduction

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.

Contents

Get started

Install

Install it from NuGet:

Install-Package Pathoschild.Http.FluentClient

The client works on most platforms (including Linux, Mac, and Windows):

platform min version
.NET 5.0
.NET Core 1.0
.NET Framework 4.5.2
.NET Standard 1.3
Mono 4.6
Unity 2018.1
Universal Windows Platform 10.0
Xamarin.Android 7.0
Xamarin.iOS 10.0
Xamarin.Mac 3.0

Basic usage

Just create the client and chain methods to set up the request/response. For example, this sends a GET request and deserializes the response into a custom Item class based on content negotiation:

Item item = await new FluentClient()
   .GetAsync("https://example.org/api/items/14")
   .As<Item>();

You can also reuse the client for many requests (which improves performance using the built-in connection pool), and set a base URL in the constructor:

using var client = new FluentClient("https://example.org/api");

Item item = await client
   .GetAsync("items/14")
   .As<Item>();

The client provides methods for DELETE, GET, POST, PUT, and PATCH out of the box. You can also use SendAsync to craft a custom HTTP request.

URL arguments

You can add any number of arguments to the request URL with an anonymous object:

await client
   .PostAsync("items/14")
   .WithArguments(new { page = 1, search = "some search text" });

Or with a dictionary:

await client
   .PostAsync("items/14")
   .WithArguments(new Dictionary<string, object> {});

Or individually:

await client
   .PostAsync("items/14")
   .WithArgument("page", 1)
   .WithArgument("search", "some search text");

Body

You can add a model body directly in a POST or PUT:

await client.PostAsync("search", new SearchOptions());

Or add it to any request:

await client
   .GetAsync("search")
   .WithBody(new SearchOptions());

Or provide it in various formats:

format example
serialized model WithBody(new ExampleModel())
form URL encoded WithBody(p => p.FormUrlEncoded(values))
file upload WithBody(p => p.FileUpload(files))
HttpContent WithBody(httpContent)

Headers

You can add any number of headers:

await client
   .PostAsync("items/14")
   .WithHeader("User-Agent", "Some Bot/1.0.0")
   .WithHeader("Content-Type", "application/json");

Or use methods for common headers like WithAuthentication, WithBasicAuthentication, WithBearerAuthentication, and SetUserAgent.

(Basic headers like Content-Type and User-Agent will be added automatically if you omit them.)

Read the response

You can parse the response by awaiting an As* method:

await client
   .GetAsync("items")
   .AsArray<Item>();

Here are the available formats:

type method
Item As<Item>()
Item[] AsArray<Item>()
byte[] AsByteArray()
string AsString()
Stream AsStream()
JToken AsRawJson()
JObject AsRawJsonObject()
JArray AsRawJsonArray()

The AsRawJson method can also return dynamic to avoid needing a model class:

dynamic item = await client
   .GetAsync("items/14")
   .AsRawJsonObject();

string author = item.Author.Name;

If you don't need the content, you can just await the request:

await client.PostAsync("items", new Item());

Read the response info

You can also read the HTTP response info before parsing the body:

IResponse response = await client.GetAsync("items");
if (response.IsSuccessStatusCode || response.Status == HttpStatusCode.Found)
   return response.AsArray<Item>();

Handle errors

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);
  • use your own error filter.

Advanced features

Request options

You can customize the request/response flow using a few built-in options.

You can set an option for one request:

IResponse response = await client
   .GetAsync("items")
   .WithOptions(ignoreHttpErrors: true);

Or for all requests:

client.SetOptions(ignoreHttpErrors: true);

The available options are:

option default effect
ignoreHttpErrors false Whether HTTP error responses like HTTP 404 should be ignored (true) or raised as exceptions (false).
ignoreNullArguments true Whether null arguments in the request body and URL query string should be ignored (true) or sent as-is (false).
completeWhen ResponseContentRead When we should stop waiting for the response. For example, setting this to ResponseHeadersRead will let you handle the response as soon as the headers are received, before the full response body has been fetched. This only affects getting the IResponse; reading the response body (e.g. using a method like IResponse.As<T>()) will still wait for the request body to be fetched as usual.

Simple retry policy

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) => TimeSpan.FromSeconds(attempt * 5) // wait 5, 10, and 15 seconds
   );

Chained retry policies

You can also wrap retry logic into IRetryConfig implementations:

/// <summary>A retry policy which retries with incremental backoff.</summary>
public class RetryWithBackoffConfig : IRetryConfig
{
    /// <summary>The maximum number of times to retry a request before failing.</summary>
    public int MaxRetries => 3;

    /// <summary>Get whether a request should be retried.</summary>
    /// <param name="response">The last HTTP response received.</param>
    public bool ShouldRetry(HttpResponseMessage response)
    {
        return request.StatusCode != HttpStatusCode.OK;
    }

    /// <summary>Get the time to wait until the next retry.</summary>
    /// <param name="attempt">The retry index (starting at 1).</param>
    /// <param name="response">The last HTTP response received.</param>
    public TimeSpan GetDelay(int attempt, HttpResponseMessage response)
    {
        return TimeSpan.FromSeconds(attempt * 5); // wait 5, 10, and 15 seconds
    }
}

Then you can add one or more retry policies, and they'll each be given the opportunity to retry a request:

client
   .SetRequestCoordinator(new[]
   {
      new TokenExpiredRetryConfig(),
      new DatabaseTimeoutRetryConfig(),
      new RetryWithBackoffConfig()
   });

Note that there's one retry count across all retry policies. For example, if TokenExpiredRetryConfig retries once before falling back to RetryWithBackoffConfig, the latter will receive 2 as its first retry count. If you need more granular control, see custom retry/coordination policy.

Cancellation tokens

The client fully supports .NET cancellation tokens if you need to cancel requests:

using var tokenSource = new CancellationTokenSource();

await client
   .PostAsync()
   .WithCancellationToken(tokenSource.Token);

tokenSource.Cancel();

The cancellation token is used for the whole process, from sending the request to reading the response. You can change the cancellation token on the response if needed, by awaiting the request and calling WithCancellationToken on the response.

Custom requests

You can make changes directly to the HTTP request before it's sent:

client
   .GetAsync("items")
   .WithCustom(request =>
   {
      request.Method = HttpMethod.Post;
      request.Headers.CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromMinutes(30) };
   });

Synchronous use

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

Extensibility

Custom formats

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 included MediaTypeFormatterBase class).

Custom filters

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", "");
}

Custom retry/coordination policy

You can implement IRequestCoordinator to control how requests are dispatched. For example, here's a 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 Task<HttpResponseMessage> ExecuteAsync(IRequest request, Func<IRequest, Task<HttpResponseMessage>> send)
   {
      HttpStatusCode[] retryCodes = { HttpStatusCode.GatewayTimeout, HttpStatusCode.RequestTimeout };
      return Policy
         .HandleResult<HttpResponseMessage>(request => retryCodes.Contains(request.StatusCode)) // should we retry?
         .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(attempt)) // up to 3 retries with increasing delay
         .ExecuteAsync(() => send(request)); // begin handling request
   }
}

...and here's how you'd set it:

client.SetRequestCoordinator(new RetryCoordinator());

(You can only have one request coordinator on the client; you should use HTTP filters instead for most overrides.)

Custom HTTP

For advanced scenarios, you can customise the underlying HttpClient and HttpClientHandler. See the next section for an example.

Mocks for unit testing

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

fluenthttpclient's People

Contributors

dependabot[bot] avatar ike86 avatar jericho avatar lostdev avatar pathoschild avatar shbita avatar timothymakkison avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fluenthttpclient's Issues

Add FluentHttpClient to nuget tags

Hi, I wanted to try out your library but had trouble finding it in the nuget search when typing in "fluent http client", and then "FluentHttpClient". The package name is different from the repo so maybe you should add FluentHttpClient to the nuget tags.

Migrate to PCL

Migrate the fluent client to Portable Class Libraries (PCL) so it can be used on multiple .NET platforms. The tentative goal is to support .NET 4.5+, Windows 8+ apps, Universal Windows Platform, ASP.NET Core, and (when it's released) .NET Platform Standard.

(This should also unblock Pathoschild/FluentJira#3.)

To do

  • Convert projects into portable libraries.
  • Remove UriExtensions.ParseQueryString, which isn't compatible with PCL.
    • In Request, use string manipulation instead to append query arguments.
    • In JsonNetFormatter, remove the JSONP compatibility code that uses it. (If a REST API provides JSONP, it probably provides JSON anyway.)
  • Fix various API changes (e.g. replace TypeDescriptor.GetProperties(...) with Type.GetRuntimeProperties()).

Exception when getting response AsString and charset is invalid

Admittedly, this is an edge case but it's something I recently faced when consuming a 3rd party API which returned a misspelled charset in their response. The issue is the built-in ReadAsStringAsync method attempts to figure out the proper encoding to use when converting the content of the response's body into a string but due to the misspelling, the HttpClient was unable to figure out the encoding and therefore, an exception was thrown.

In the case I faced, it was a very simple misspelling: the charset was utf8 instead of utf-8. I pointed this out to the vendor; they acknowledged the misspelling but they declined to fix it. Their justification was that some languages where able to handle the misspelling. The fact that .Net can't handle an invalid charset did not bother them.

Fortunately, I was able to figure out a work around and wrote the following extension method:

public static async Task<string> ReadAsStringAsync(this HttpContent content, Encoding encoding)

You can either pass a specific encoding to be used to decode the content into a string OR pass a null value which will cause the method to attempt to figure out the appropriate encoding (which mimics HttpClient.ReadAsStringAsync default behavior) and will default to Encoding.UTF8 in case we can't figure out the encoding.

The source code is available in this gist.

Here's what I propose:

  1. include my extension method in the FluentHttpClient library
  2. replace the call to ReadAsStringAsync() in the AsString() method in Request.cs with ReadAsStringAsync(null)

I will submit a PR

ApiException

This is more a question that an issue: did you design FluentClient to purposefully throw an exception whenever the HttpResponse is anything other than HTTP200?

The reason I ask is that with the standard HttpClient I would receive a response, check the StatusCode and decide what to do. For example, if the status code is 429 (which means 'TOO MANY REQUESTS') I have code to automatically pause and reissue the request. With FluentClient I think my logic will have to be different: I think I need to try ... catch the call var response = await _fluentClient.SendAsync(httpRequest); and check the status code in the catch block.

Also, can you think of a better design? For instance, is there a way to write a IFilter that would check the statuscode in the 'OnResponse' and reissue the request if necessary?

HttpClient should not be disposed if provided

Say I have the following code in my app:

var httpClient = new HttpClient(new HttpClientHandler()
{
    Proxy = new WebProxy(...)
});
using (var fluentClient = new FluentClient("http://example.org/api/", httpClient))
{
    ... do something with fluentClient ...
}
var response = await httpClient.SendAsync(myHttpRequest);

This last line will fail because my httpClient was disposed by FluentHttpClient in the Dispose(bool isDisposing) method. If the HtpClient is not passed to the FluentClient constructor, it makes sense for this HttpClient to be disposed. However, FluentClient should never dispose a value that is provided by the developer.

I suggest the following:

  1. Add a private field to the FluentClient class: private readonly bool _mustDisposeHttpClient;
  2. Modify the FluentClient constructors like so:
       public FluentClient(string baseUri)
            : this(baseUri, null) { }

        public FluentClient(string baseUri, HttpClient client)
        {
            _mustDisposeHttpClient = client == null;
            this.BaseClient = client ?? new HttpClient();
            this.Filters = new List<IHttpFilter> { new DefaultErrorFilter() };
            if (baseUri != null)
                this.BaseClient.BaseAddress = new Uri(baseUri);
            this.Formatters = new MediaTypeFormatterCollection();
        }
  1. Finally, modify the Dispose method like so:
       protected virtual void Dispose(bool isDisposing)
        {
            if (this.IsDisposed)
                return;

            if (isDisposing && _mustDisposeHttpClient)
                this.BaseClient.Dispose();

            this.IsDisposed = true;
        }

I will be happy to submit a PR.

Support for multiple query parameters with the same name

Looking at Request.GetArguments, which is called from WithArguments, it looks like I need to pass either an object or an IDictionary to the method in order to setup the query string.

Unfortunately, the API I'm calling requires me to enter the same parameter with multiple different values.
Ex: http://service/foo?bar=1&bar=2

Would it be problematic to use IEnumerable<KeyValuePair<>> Instead of IDictionary within that class?

Hardcoded User-Agent

I see two problems with the user-agent:

  1. The value is hard-coded (in Factory.cs, on line 45) and I can't provide my own string if desired. I understand the need for a default value in case the developer doesn't care about this string, but I should be able to override it if I want.
  2. The current hard coded value starts with: FluentHttpClient/0.4. The problem is that the version number is hard coded and does not reflect the actual version number of the FluentHttpClient library.

I propose the following:

  1. Remove the hard-coded user-agent string
  2. Add a new UserAgentFilter class. This class will, by default, use ""FluentHttpClient/x.x.x (+http://..." and x.x.x will be automatically replaced with the FluentClient assembly version.
  3. Add a new public void SetUserAgent(string useragent) method to IClient and Client.cs which will replace the default user agent filter with one that uses the provided custom string

I will be happy to submit a PR

Unable to retry requests when content is not null

The PR I submitted in January 2017 to introduce 'retry' functionality included an extension method to clone the HttpRequestMessage before dispatching it to avoid The request message was already sent. Cannot send the same request message multiple times.. exception when re-attempting to dispatch the request.

This extension method works fine for scenarios where a request does not have any body content, such as a GET for example, but for scenarios like POST which typically have content in the body, the extension method does not work properly.

When the request is dispatched for the second time, you get a Cannot access a disposed object. exception which is due to the fact that the content stream is automatically disposed after the initial request.

The solution is to make sure to clone the content (if any) when cloning the request.

I will submit a PR shortly with the solution.

Add dynamic/JSON methods

The client supports parsing content as a strongly-typed model (or array), bytes, string, or stream. Sometimes it's useful to parse content into a data structure without creating a strongly-typed model. Json.NET lets you do that by parsing as JObject, JArray, or dynamic:

// JObject
JObject content = JObject.Parse("{ Field: true }");
bool value = content.Value<bool>("Field");

// JArray
JArray content = JArray.Parse("[ {Field: true} ]");
bool value = content[0].Value<bool>("Field");

// dynamic (object)
dynamic content = JObject.Parse("{ Field: true }");
bool value = content.Field;

// dynamic (array)
dynamic content = JArray.Parse("[ {Field: true} ]");
bool value = content[0].Field;

Consider adding equivalent methods to the fluent client for JSON only:

JObject content = client.GetAsync().AsJsonObject();
JArray content = client.GetAsync().AsJsonArray();

(The dynamic support is part of JObject and JArray.)

Rethink error-handling

The client throws exceptions by default for non-success responses. Should we rethink the default error-handling logic?

Migrate content negotiation to .NET Standard

Added Pathoschild.Http.FluentClient to an ASP .NET Core 2.0 WebAPI project.

Called PostAsync and received:

System.Resources.MissingManifestResourceException: 'Could not find any resources appropriate for the specified culture or the neutral culture. Make sure "System.Net.Http.Properties.Resources.resources" was correctly embedded or linked into assembly "WinInsider.System.Net.Http.Formatting" at compile time, or that all the satellite assemblies required are loadable and fully signed.'

Packages installed:

<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.0.0" />

Migrate to pure .NET Standard

We target both .NET Standard and .NET Framework. Since .NET Framework projects can in theory reference .NET Standard libraries, look into just migrating fully to .NET Standard for simplicity.

Set cookies in request?

Is there any ways to manually set a specific cookie in request, or maybe a way to maintain a cookiejar across multiple requests?

If my tests are correct, HTTPClient.GetAsync(...).WithHeader("Cookie", "A=B") won't actually set cookie.

NuGet Dependency

Due to:

Error 1 The type 'System.Net.Http.Formatting.MediaTypeFormatter' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Net.Http.Formatting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'.

Recommend that you place a dependency on this assembly in the NuGet package(s)

x-www-form-urlencoded

I have a vendor that requires me to POST my requests to their API and they want parameters to be encoded as a web form. They accept neither JSON nor XML, only web forms. Do you think this can be handled by FluentHttpClient?

For reference, here's how I currently handle it with a HttpClient:

public async Task<HttpResponseMessage> ExecuteRequestAsync(string endpoint, IEnumerable<KeyValuePair<string, object>> parameters, CancellationToken cancellationToken = default(CancellationToken))
{
    var content = (StringContent)null;
    if (parameters != null)
    {
        var paramsWithValue = parameters.Where(p => p.Value != null).Select(p => string.Concat(Uri.EscapeDataString(p.Key), "=", Uri.EscapeDataString(p.Value.ToString())));
        var paramsWithoutValue = parameters.Where(p => p.Value == null).Select(p => string.Concat(Uri.EscapeDataString(p.Key), "="));
        var allParams = paramsWithValue.Union(paramsWithoutValue).ToArray();
        content = new StringContent(string.Join("&", allParams), Encoding.UTF8, "application/x-www-form-urlencoded");
    }

    var httpRequest = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri(BaseUrl, endpoint),
        Content = content
    };
    var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
    return response;
}

Make some methods overridable

Make Post and Send virtual, so that we can override them and include parameters that need to be included everywhere

IE:

public override IRequestBuilder Post(string resource, T body)
{
return base.Post(resource, body)
.WithArgument("outputFormat", "JSON");
}

Refactor the FluentClient constructor

I suggest combining the two overloaded constructors:

public FluentClient(string baseUri)
public FluentClient(string baseUri, HttpClient client)

into a single one:

public FluentClient(string baseUri, HttpClient client = null)

Also, I suggest adding a new constructor that allows specifying the baseUri as Uri instead of a string:

public FluentClient(Uri baseUri, HttpClient client = null)

I will submit a PR.

Client interface not as simple as could be

In a previous issue, we discussed our intent to keep the client interface as simple as possible. Specifically, we discussed the fact that we don't want to add many methods to this interface. and I completely agree on this point.

However, I would argue that the current interface is not as simple as can be and there are some methods that shouldn't be on this interface. For instance, GetAsync, DeleteAsync and PutAsync exist simply for the developer's convenience, they invoke another method in the interface (SendAsync in this case) and hide some on the mundane details of the parameters that need to be passed to this other method. One thing important to note is that these methods do not introduce any new functionality. (by the way, since these methods exist solely to simplify how your invoke another method, I call them "convenience" methods).

Therefore I think these methods should be removed from the interface and should exist in the FluentHttpClient project as extension methods.

Conceptually, this is a similar situation than when I added SetAuthentication to the Client interface and I added SetBasicAuthentication and SetBearerAuthentication as extension methods in order not to clutter the interface with "convenience" methods.

I will submit a PR to demonstrate what I'm talking about.

Retry

It would be convenient to have a way to automatically retry http requests when necessary. The best example I can think of is: some APIs will return a HTTP 429 status code (which means 'TOO MANY REQUESTS') when you exceed the number of request within a certain period of time. The vendor will typically tell you to wait a little bit before retrying you call. Some vendors even include how long you should wait in the response. Another example, is when the API returns a code that indicate a transient problem occurred and you should retry your call.

I propose the following:

  1. Add a IRetryStrategy interface. This interface is really simple; only two methods: ShouldRetry and GetNextDelay.
  2. Provide a default implementation called NoRetry which does not retry. This would match the library's current behavior.
  3. Add SetRetryStrategy(IRetryStrategy retryStrategy) method on 'IClient' which allows specifying the retry strategy used for all subsequent calls
  4. Add WithRetryStrategy(IRetryStrategy retryStrategy) method on 'IRequest' which allows specifying the retry strategy used for the current call
  5. Developers can create new classes based on IRetryStrategy to implement their own retry logic. Their class determines which calls can be retried, the max number of time a call can be retried, how long to pause between each attempt, etc.

I will submit a PR. I can also provide a custom retry strategy I used on a previous project for illustration purposes.

Polish & release 3.2

This is just a tracking ticket to finalise the 3.2 release:

  • Ensure all unit tests pass (for all target frameworks).
  • Ensure code conventions are applied, all code is documented, etc.
  • Polish documentation, release notes, etc.
  • Prepare NuGet package.
  • Test the client in a few projects.
  • Deploy to NuGet.

Feel free to comment if there's anything else you want to include in the 3.2 release.

Additional extension methods

I wrote a few simple extension methods that I'm using in my project. For the most part, they are simple one-liners that make it easier to perform repetitive tasks.

Let me know if you think it would be useful to merge them into FluentHtpClient.

// The APIs I interact with accept JSON 
public static IRequest WithJsonBody<T>(this IRequest request, T body);

// Use the PATCH method when dispatching a request
public static IRequest PatchAsync(this Pathoschild.Http.Client.IClient client, string resource);

// This solves the issue I discussed in #20. At the time you were not interested in implementing a fix, but I'm mentioning it again in case you changed your mind ;-)
public static Task<string> AsString(this IResponse response, Encoding encoding);
public static async Task<string> AsString(this IRequest request, Encoding encoding);

Async methods should accept a concellation token

The cancellation token allows a long running async method to be cancelled but the FluentClient currently does not accept such a token. I propose adding a parameter for the token to methods such as DeleteAsync, GetAsync, SendAsync, etc.. Of course this parameter should be optional.

I will submit a PR.

Use consistent project name

The project name is inconsistent:

source current value
namespace Http.Client
class name FluentClient
name in readme FluentHttpClient
package ID Http.FluentClient
(can't change this)
package name Fluent HTTP Client
repo name FluentHttpClient

Consider using a more consistent name.

OData feeds raise an exception

example:

        var url = "http://services.odata.org/V4/Northwind/Northwind.svc/";
        IClient client = new FluentClient(url);
        var customers = await client
				   .GetAsync("Customers")
				    .AsArray<Customer>(); 
        Console.WriteLine(item.Length);

Error Message:

   InnerException: 
     HResult=-2146233088
   Message=Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'NorthwindModel.V4.Customer[]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
       To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized  type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
       Path '['@odata.context']', line 1, position 18.
       Source=Newtonsoft.Json

Also, I can't capture the exception when using try .. catch{}. The exception isn't reached.

It seems that the default json deserializer can't handle arrays in OData feeds, because OData feeds are different than REST services.

Edit:
Customer is POCO

	public class Customer
		{
			public string CustomerID { get; set; }  
			public string CompanyName { get; set; }  
			public string ContactName { get; set; }
			public string ContactTitle { get; set; }
			public string Address { get; set; }
			public string City { get; set; }
			public string Region { get; set; }
			public string PostalCode { get; set; }
			public string Country { get; set; }
			public string Phone { get; set; }
			public string Fax { get; set; }
		}

Downgrade System.Net.Http

The NETFULL (i.e. .net 4.5.2) target of my library currently references the System.Net.Http nuget package which I intend to replace with FluentHttpClient as soon as the next release with "retries" is publicly available. A user recently opened a ticket to complain that he can't use my library because System.Net.Http 4.0.0 requires a version of the nuget client that is not available in Visual Studio 2013 (he says he is in a corporate environment and can't upgrade to VS.NET 2015). The solution is to downgrade System.Net.Http to 2.0.20710 which works for him and since other users are free to use a more recent version if they desire, this solution seems to satisfy all my users. I was not able to find release notes so I don't know specifically what's different but my (somewhat) educated guess is that supporting NETSTANDARD and unsealing classes for easier unit testing are the major changes. So far I haven't noticed any other significant change between 2.x and 4.x

However, this solution will go out the window when I start referencing FluentHttpClient because it's NETFULL target references System.Net.Http 4.0.0. So my question is: what do you think about downgrading the reference to 2.0.20710?

Request WithArguments - no support for repeated arguments in query string

When we have API's with query string parameters that are enumerables/lists, the standard way to specify them in the request is by repeating the parameter in the query string with different values; eg.:

http://api.com/Movies?genre=action&genre=drama

If we try to call WithArguments with a KeyValuePair<string, string> with the values of the example above, it throws an exception of duplicated key, since internally this is converted to a dictionary.

A workaround is doing a foreach of this and calling WithArgument for each one individually.

What is the expected request URL?

Consider the following scenario:

var fluentClient = new FluentClient("http://api.vendor.com/v3/");
var request = client.Get("/myresource");

What is the expected request URL?

I'm asking because I expect the request to be sent to http://api.vendor.com/v3/myresource but instead the request is dispatched to http://api.vendor.com/myresource. The reason is because the resource starts with a slash. Of course, I could simply remove the slash and move on but I wanted to check what you think the request URL should be and whether this is an issue that must be addressed.

Review design decisions

The next version will be 3.0 since we have breaking changes as part of #22 and #31. Since we're already incrementing the major version, review the design decisions taken in earlier versions in case there are other breaking changes we should consider.

To do

  • AsList<T>() should return T[] instead of List<T>.
  • AsList<T>() should be named AsArray<T>().
  • Eliminate obsolete JsonNetFormatter.
  • Simplify IHttpFilter by removing message arguments (already available via IRequest and IResponse).

Remove System.Net.Http.Formatting dependency

What do you think about removing the dependencies on Microsoft.AspNet.WebApi.Client and WinInsider.System.Net.Http.Formatting and replacing with our own code?

I did a preliminary review and it looks like we use the following classes from these packages:

  • MediaTypeFormatter: abstract class that describes a formatter (FluentClient already has a MediaTypeFormatter abstract class which is extremely similar)
  • MediaTypeFormatterCollection: very simple wrapper around Collection<MediaTypeFormatter> which makese sure the three default formatters are automatically added when a new instance of this collection is created
  • JsonMediaTypeFormatter: there used to be a class in the FluentClient project which was marked as obsolete and recently removed from the project. Maybe we could bring it back?
  • XmlMediaTypeFormatter: XML formatter
  • FormUrlMediaTypeFormatter: Form Url encoded formatter
  • ObjectContent: takes a formatter and an object and converts it into a string to be submitted

And also, the following extension methods:

  • CreateResponse(this HttpRequestMessage request, HttpStatusCode status);
  • ReadAsAsync<T>(this HttpContent content, MediaTypeFormatterCollection formatters);

I'm continuing my research to figure out if we use more classes and also working on a quick POC to show that it wouldn't be too complicated.

There's no easy way to specify authentication

The readme says that I can create my own implementation of IHttpFilter for authentication but this means that every developer using FluentHttpClient must "reinvent the wheel" for this simple task. Since most API require some form of authentication, I believe FluentHttpClient should make it easier to provide authentication.

I propose the following:

We should be able to configure authentication for each request

  1. I suggest adding IRequest WithAuthentication(string scheme, string value); to IRequest (and Request.cs of course).
  2. Add convenient methods for the most common authentication schemes. The two that I have most frequently seen are: 'bearer' (which is when you provide an api key) and 'basic' (which is when you provide a username and password). These two methods would be called WithBearerAuthentication and WithBasicAuthentication and would simply invoke the WithAuthentication method with appropriate parameters.

We should also be able to configure authentication for the client

  1. I suggest adding a new IHttpFilter called AuthorizationFilter. This filter would automatically add the authentication to every request
  2. I suggest adding public void AddAuthentication(string scheme, string value); to IClient (and FluentClient.cs of course). This method would instantiate a new AuthenticationFilter with provided parameters and add it to the list of filters.
  3. Add convenient methods for the most common authentication schemes. As I previously mentioned, the two that I think are the most common are 'bearer' and 'basic'. These two methods would be called AddBearerAuthentication and AddBasicAuthentication and would simply invoke the AddAuthentication method with appropriate parameters.

I will be away for a while but I volunteer to submit a PR to add this functionality when I return.

Default headers

I am porting my second library to FluentHttpClient and noticed a missing feature: I can't set a default header that gets automatically added to every request. I know that I can invoke the IRequest.WithHeader(string key, string value) for each request, but it's very tedious.

I propose adding a method to IFluentClient like so:

        /// <summary>Adds a header which should be sent with each request.</summary>
        /// <param name="name">The name of the header.</param>
        /// <param name="value">The value of the header.</param>
        public IClient SetHeader(string name, string value)
        {
            this.BaseClient.DefaultRequestHeaders.Add(name, value);
            return this;
        }

Still maintained?

Hi @Pathoschild,

just curious if you are still maintaining this project? I haven't seen any activity in a while so I was wondering. If you are, any chance you can look at the PR I submitted last week regarding issue with the retry coordinator?

Thanks

Custom diagnostic IHttpFilter

I am trying to write a custom IHttpFilter to log diagnostic info such as the time it took for the request to complete. The following is my first draft:

public class DiagnosticHandler : IHttpFilter
{
    private StopWatch _requestTimer;

    public void OnRequest(IRequest request)
    {
        _requestTimer = Stopwatch.StartNew();
    }

    public void OnResponse(IResponse response, bool httpErrorAsException)
    {
        _requestTimer.Stop();
        Logger.Debug(string.Format("Response elapsed time: {0} ms", _requestTimer.ElapsedMilliseconds));
    }
}

I'm afraid that this will not work so well if multiple requests are dispatched since filters are shared across multiple requests and therefore the value in _requestTimer will be overwritten. Can you think of a better way to ensure each request/response get their own timer?

how to use client with TestSever

Is there a way to you asp.net core TestServer?

` var builder = new WebHostBuilder()
.UseStartup();
var _server = new TestServer(builder);

        _handler = _server.CreateHandler();
        _client = _server.CreateClient();`

Polish & release 3.0

This is just a tracking ticket to finalise the 3.0 release:

  • Ensure code conventions are applied, all code is documented, etc.
  • Ensure all unit tests pass.
  • Polish documentation, release notes, etc.
  • Test the client in a few projects.
  • Deploy to NuGet.

Retrying too many times (3.1 beta)

While testing release 3.1 I noticed that one of my project's unit tests is failing. My test is checking that FluentHtpClient is retrying exactly 5 times but it appears that it's retrying 6 times.

[Fact]
public async Task Retry_failure()
{
	// Arrange
	var baseUri = "https://api.vendor.com/";
	var resource = "testing";

	var mockHttp = new MockHttpMessageHandler();
	mockHttp.Expect(HttpMethod.Get, baseUri + resource).Respond((HttpStatusCode)429);
	mockHttp.Expect(HttpMethod.Get, baseUri + resource).Respond((HttpStatusCode)429);
	mockHttp.Expect(HttpMethod.Get, baseUri + resource).Respond((HttpStatusCode)429);
	mockHttp.Expect(HttpMethod.Get, baseUri + resource).Respond((HttpStatusCode)429);
	mockHttp.Expect(HttpMethod.Get, baseUri + resource).Respond((HttpStatusCode)429);

	var httpClient = mockHttp.ToHttpClient();
	var client = new FluentClient(baseUri, httpClient);
	client.SetRequestCoordinator(
		maxRetries: 5,
		shouldRetry: res => res.StatusCode == (HttpStatusCode)429,
		getDelay: (attempt, res) => TimeSpan.Zero
	);

	// Act
	var result = await Should.ThrowAsync<Exception>(async () => await client.SendAsync(HttpMethod.Get, resource).AsResponse().ConfigureAwait(false)).ConfigureAwait(false);

	// Assert
	mockHttp.VerifyNoOutstandingExpectation();
	mockHttp.VerifyNoOutstandingRequest();

	result.Message.ShouldBe("The HTTP request failed, and the retry coordinator gave up after the maximum 5 retries");
}

This unit test was working fine in 3.0 but it is now throwing the following exception: No matching mock handler. My guess is that the logic in the retry coordinator has changed and instead of attempting the request 5 times (which was the logic in 3.0) it is now attempting the request 5 times IN ADDITION to the initial attempt (for a total of 6 attempts).

Missing methods for authentication on IClient interface

In PR #15 I added SetAuthentication to IClient and WithAuthentication to IRequest. I also added two convenient extension methods WithBasicAuthentication and WithBearerAuthentication to IRequest.

However, I forgot to add similar extension methods to the IClient interface.

Will submit PR.

Polish & release 3.1

This is just a tracking ticket to finalise the 3.1 release:

  • Ensure all unit tests pass.
  • Ensure code conventions are applied, all code is documented, etc.
  • Polish documentation, release notes, etc.
  • Prepare NuGet package.
  • Test the client in a few projects.
  • Deploy to NuGet.

Feel free to comment if there's anything else you want to include in the 3.1 release.

Base URLs don't have implicit ending slash in some cases

The base URL's implicit end / in #46 doesn't seem to be working in some cases:

// works fine
await new FluentClient("https://api.smapi.io/v2.0/").GetAsync("mods");

// HTTP 404 (becomes https://api.smapi.io/mods)
await new FluentClient("https://api.smapi.io/v2.0").GetAsync("mods");

Also write unit tests for the base URL logic.

Request interface not as simple as could be

The IRequest interface has three overloads for the WithBody method (to be more accurate: there are 2 overloads and the third method is called WithBodyContent). Only WithBodyContent should be on the interface and the other two methods should be extension methods since they invoke WithBodyContent.

This is similar to the work that was done to simplify the IClient interface as discussed in #26.

Will submit PR.

Xamarin.iOS not working with 3.2

We have problems to run the client on iOS devices (and simulators).

We get the following error, if we try to use the client (simple get on google page).

// Message
The type initializer for 'System.Net.Http.FormattingUtilities' threw an exception.

// Stacktrace
  at System.Net.Http.Formatting.JsonMediaTypeFormatter..ctor () [0x0000b] in <cfa5c25867be4a27a5f6292084f06ede>:0 
  at System.Net.Http.Formatting.MediaTypeFormatterCollection.CreateDefaultFormatters () [0x00000] in <cfa5c25867be4a27a5f6292084f06ede>:0 
  at System.Net.Http.Formatting.MediaTypeFormatterCollection..ctor () [0x00000] in <cfa5c25867be4a27a5f6292084f06ede>:0 
  at Pathoschild.Http.Client.FluentClient..ctor (System.Uri baseUri, System.Net.Http.HttpClient client) [0x00052] in <ff5fa7974da7431ebe2fb66e443b1576>:0 
  at Pathoschild.Http.Client.FluentClient..ctor (System.String baseUri, System.Net.Http.HttpClient client) [0x00007] in <ff5fa7974da7431ebe2fb66e443b1576>:0 
  at FluentApiTest.MainPage+<Handle_Clicked>d__1.MoveNext () [0x00018] in /Users/er/Projects/FluentApiTest/FluentApiTest/MainPage.xaml.cs:23

And following inner exception:

// Message
The method or operation is not implemented.

// Stacktrace
at System.Runtime.Serialization.XsdDataContractExporter..ctor () [0x00006] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.10.1.178/src/Xamarin.iOS/mcs/class/System.Runtime.Serialization/ReferenceSources/XsdDataContractExporter_mobile.cs:39 
  at System.Net.Http.FormattingUtilities..cctor () [0x000e4] in <cfa5c25867be4a27a5f6292084f06ede>:0

You can simple reproduce the behaviour by creating a defaul Xamain.Forms or Xamarin.iOS project and try:

var client = new FluentClient("https://www.google.com");
var result = await client.GetAsync("/").AsString();

Make Request.Filters public

This is related to discussion #38, but I thought it warranted a separate ticket.

Is there a reason Request.Filters is private instead of being public? I'm asking because I have a custom error handler that I need for all the calls to the vendor API except for one specific request where I need to remove this error handler. I still need the default error handler in case the vendor API throws HTTP 500 or something, but I need to turn off my custom logic. Ideally, I would like to be able to write:

var request = _client
	.GetAsync(endpoint)
	.WithCancellationToken(cancellationToken);

request.Filters.Remove<AcmeErrorHandler>();

var myObject= await request.As<AcmeObject>().ConfigureAwait(false);

I propose the following:

  1. Add Filters to the IRequest interface
  2. Change Request.Filters to public

This would be similar to the IRequest.Formatters public property.

Please note that the WithHttpErrorAsException discussed in #38 would not help me in this situation since I want the default error handler to be enabled. It's just my custom handler that needs to be disabled.

Retry coordinator should handle timeouts

The retry coordinator doesn't retry on timeout since that causes an exception instead an HTTP error response:

System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task 1.GetResultCore(Boolean waitCompletionNotification)
at System.Threading.Tasks.Task 1.get_Result()
at xxx.xx.x.x.QueryPrivate[T](ApiClient apiClient, String myHTTPMethod, String _RelativeURL, Int32 _myTimeOut, Object _payload) in zzz.cs:line 337

Reported via #58 and Stack Overflow.

Filters property missing from IClient interface

I noticed that the Filters property is on the FluentClient class but not on the IClient interface. Is that intentional or an oversight? What makes me think it is probably an oversight is that a similar property called Formatters is on the IClient interface and also on the FluentClient class (which I think is correct).

If it's simply an oversight, I'll be happy to submit a PR to correct it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.