Coder Social home page Coder Social logo

sketch7 / fluentlyhttpclient Goto Github PK

View Code? Open in Web Editor NEW
112.0 3.0 16.0 511 KB

Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible.

License: MIT License

Shell 0.78% C# 99.22%
csharp http-client fluent-api netstandard httpclient fluentapi middleware rest rest-client dotnet

fluentlyhttpclient's Introduction

Fluently Http Client

CI NuGet version

Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible.

Quick links

Change logs | Project Repository

Features

  • Fluent APIs
  • Highly extensible
  • Middleware Support
    • Custom Classes with DI enabled
    • Access to both Request/Response within same scope (similar to ASPNET middleware)
    • Logger and Timer middleware out of the box
  • Multiple HttpClient support with a Fluent API for Client builder
  • Customizable Formatters (JSON, XML out of the box)
  • Url interpolation and query params e.g. person/{id} / person?id=1
  • GraphQL support
  • File upload support

Installation

Available for .NET Standard 2.0+

NOTE: 1.x depends on .NET Standard 1.4+, use that if you need older .NET standard.

NuGet

PM> Install-Package FluentlyHttpClient

csproj

<PackageReference Include="FluentlyHttpClient" Version="*" />

Table of Contents

Usage

Configure

Add services via .AddFluentlyHttpClient().

// using Startup.cs (can be elsewhere)
public void ConfigureServices(IServiceCollection services)
{
    services.AddFluentlyHttpClient();
}

Configure an Http client using the Http Factory (you need at least one).

// using Startup.cs (can be elsewhere)
public void Configure(IApplicationBuilder app, IFluentHttpClientFactory fluentHttpClientFactory)
{
  fluentHttpClientFactory.CreateBuilder(identifier: "platform") // keep a note of the identifier, its needed later
    .WithBaseUrl("http://sketch7.com") // required
    .WithHeader("user-agent", "slabs-testify")
    .WithTimeout(5)
    .UseMiddleware<LoggerHttpMiddleware>()
    .Register(); // register client builder to factory
}

Basic usage

Simple API

Simple API (non-fluent) is good for simple requests as it has a clean, minimal API.

// inject factory and get client
var httpClient = fluentHttpClientFactory.Get(identifier: "platform");

// HTTP GET + deserialize result (non-fleunt API)
Hero hero = await httpClient.Get<Hero>("/api/heroes/azmodan");

// HTTP POST + deserialize result (non-fleunt API)
Hero hero = await httpClient.Post<Hero>("/api/heroes/azmodan", new
    {
        Title = "Lord of Sin"
    });

Fluent Request API

Fluent request API (request builder) allows to create more complex requests and provides further control on the response.

// inject factory and get client
var httpClient = fluentHttpClientFactory.Get(identifier: "platform");

// HTTP GET + return response and deserialize result (fluent API)
FluentHttpResponse<Hero> response = 
  await httpClient.CreateRequest("/api/heroes/azmodan")
    .ReturnAsResponse<Hero>(); // return with response

// HTTP POST + return response and deserialize result (fluent API)
Hero hero = await httpClient.CreateRequest("/api/heroes/azmodan")
    .AsPost()
    .WithBody(new
    {
        Title = "Lord of Sin"
    })
    .Return<Hero>(); // return deserialized result directly

Fluent Http Client Builder

Http client builder is used to configure http clients in a fluent way.

Register to Factory

var clientBuilder = fluentHttpClientFactory.CreateBuilder(identifier: "platform")
    .WithBaseUrl("http://sketch7.com");
fluentHttpClientFactory.Add(clientBuilder);

// or similarly via the builder itself.
clientBuilder.Register().

Register multiple + share

There are multiple ways how to register multiple http clients. The following is a nice way of doing it:

fluentHttpClientFactory.CreateBuilder("platform")
    // shared
    .WithHeader("user-agent", "slabs-testify")
    .WithTimeout(5)
    .UseTimer()
    .UseMiddleware<LoggerHttpMiddleware>()

    // platform
    .WithBaseUrl("https://platform.com")
    .Register()

    // big-data - reuse all above and replace the below
    .Withdentifier("big-data")
    .WithBaseUrl("https://api.big-data.com")
    .Register();

Create Http Client from Client

Its also possible to create a new http client from an http client, sort of sub-client which inherits options from its creator. This might be good to pass defaults for a specific endpoint.

var httpClient = factory.Get("platform");
var paymentsClient = httpClient.CreateClient("payments")
  .WithHeader("X-Gateway", "xxx")
  .WithTimeout(30)
  .Build();

Configure defaults for Http Clients

Its also possible to configure builder defaults for all http clients via ConfigureDefaults within IFluentHttpClientFactory. See example below.

fluentHttpClientFactory.ConfigureDefaults(builder
    => builder.WithUserAgent("sketch7")
        .WithTimeout(5)
);

Http Client Builder extra goodies

// message handler - set HTTP handler stack to use for sending requests
var mockHttp = new MockHttpMessageHandler();
httpClientBuilder.WithMessageHandler(mockHttp);

// request builder defaults - handler to customize defaults for request builder
httpClientBuilder.WithRequestBuilderDefaults(builder => builder.AsPut());

// formatters - used for content negotiation, for "Accept" and body media formats. e.g. JSON, XML, etc...
httpClientBuilder.ConfigureFormatters(opts =>
    {
      opts.Default = new MessagePackMediaTypeFormatter();
      opts.Formatters.Add(new CustomFormatter());
    });

Re-using Http Client from Factory

As a best practice rather than using a string each time for the identifier, it's better to create an extension method for it.

public static class FluentHttpClientFactoryExtensions
{
    public static IFluentHttpClient GetPlatformClient(this IFluentHttpClientFactory factory)
        => factory.Get("platform");
}

Request Builder

Request builder is used to build http requests in a fluent way.

Usage

LoginResponse loginResponse =
  await fluentHttpClient.CreateRequest("/api/auth/login")
    .AsPost() // set as HTTP Post
    .WithBody(new
    {
        Username = "test",
        Password = "test"
    }) // serialize body content
    .WithSuccessStatus() // ensure response success status
    .Return<LoginResponse>(); // send, deserialize result and return result directly.

Query params

requestBuilder.WithQueryParams(new
    {
        Take = 5,
        Roles = new List<string> { "warrior", "assassin" },
    }, opts => {
        opts.CollectionMode = QueryStringCollectionMode.CommaSeparated;
        opts.KeyFormatter = key => key.ToLower();
    }); // => /url?roles=warrior,assassin&take=5

Interpolate Url

requestBuilder.WithUri("{Language}/heroes/{Hero}", new
    {
        Language = "en",
        Hero = "azmodan"
    }); // => /en/heroes/azmodan

ReturnAsReponse, ReturnAsResponse<T> and Return<T>

// send and returns HTTP response
FluentHttpResponse response = requestBuilder.ReturnAsResponse();

// send and returns HTTP response + deserialize and return result via `.Data`
FluentHttpResponse<Hero> response = requestBuilder.ReturnAsResponse<Hero>();

// send and returns derserialized result directly
Hero hero = requestBuilder.Return<Hero>();

GraphQL

FluentlyHttpClient โค๏ธ GraphQL. First class support for GraphQL to be able to create request/response even simpler.

// configure globally to use uri for GraphQL endpoint.
httpClientBuilder.WithRequestBuilderDefaults(requestBuilder => requestBuilder.WithUri("api/graphql"));

// send and returns HTTP response + deserialize and return result via `.Data` directly
FluentHttpResponse<Hero> response =
  await fluentHttpClient.CreateGqlRequest("{ hero {name, title } }")
    .ReturnAsGqlResponse<Hero>();
    // => response.Data.Title

Middleware

Middleware's are used to intercept request/response to add additional logic or alter request/response.

Implementing a middleware for the HTTP client is quite straight forward, and it's very similar to ASP.NET Core middleware.

These are provided out of the box:

Middleware Description
Timer Determine how long (timespan) requests takes.
Logger Log request/response.

Two important points to keep in mind:

  • The first argument within constructor has to be FluentHttpMiddlewareDelegate which is generally called next.
  • The second argument within constructor has to be FluentHttpMiddlewareClientContext which is generally called context,
  • During Invoke the await _next(context); must be invoked and return the response, in order to continue the flow.

The following is the timer middleware implementation (bit simplified).

public class TimerHttpMiddleware : IFluentHttpMiddleware
{
    private readonly FluentHttpMiddlewareDelegate _next;
    private readonly TimerHttpMiddlewareOptions _options;
    private readonly ILogger _logger;

    public TimerHttpMiddleware(
      FluentHttpMiddlewareDelegate next, // this needs to be here and should be first
      FluentHttpMiddlewareClientContext context, // this needs to be here and should be second
      TimerHttpMiddlewareOptions options,
      ILoggerFactory loggerFactory
    )
    {
        _next = next;
        _options = options;
        _logger = loggerFactory.CreateLogger($"{typeof(TimerHttpMiddleware).Namespace}.{context.Identifier}.Timer");
    }

    public async Task<FluentHttpResponse> Invoke(FluentHttpMiddlewareContext context)
    {
        var watch = Stopwatch.StartNew();
        var response = await _next(context); // this needs to be invoked to continue middleware flow
        var elapsed = watch.Elapsed;
        _logger.LogInformation("Executed request {request} in {timeTakenMillis}ms", context.Request, elapsed.TotalMilliseconds);
        response.SetTimeTaken(elapsed);
        return response;
    }
}

namespace FluentlyHttpClient
{
    // Response extension methods - useful to extend FluentHttpResponse
    public static class TimerFluentResponseExtensions
    {
        private const string TimeTakenKey = "TIMER_TIME_TAKEN";

        public static void SetTimeTaken(this FluentHttpResponse response, TimeSpan value)
          => response.Items.Add(TimeTakenKey, value);

        public static TimeSpan GetTimeTaken(this FluentHttpResponse response)
          => (TimeSpan)response.Items[TimeTakenKey];
    }

    // FluentHttpClientBuilder extension methods - add
    public static class FluentlyHttpMiddlwareExtensions
    {
        public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions options = null)
            => builder.UseMiddleware<TimerHttpMiddleware>(options ?? new TimerHttpMiddlewareOptions());
    }
}

// response extension usage
TimeSpan timeTaken = response.GetTimeTaken();

Middleware options

Options to middleware can be passed via an argument. Note it has to be the second argument within the constructor.

public TimerHttpMiddleware(
  FluentHttpMiddlewareDelegate next,
  FluentHttpMiddlewareClientContext context,
  TimerHttpMiddlewareOptions options, // <- options should be here
  ILoggerFactory loggerFactory
)

Options can be passed when registering a middleware.

Use a middleware

fluentHttpClientFactory.CreateBuilder("platform")
    .UseMiddleware<LoggerHttpMiddleware>() // register a middleware (without args)
    .UseMiddleware<TimerHttpMiddleware>(new TimerHttpMiddlewareOptions
      {
          WarnThreshold = TimeSpan.Zero
      }) // register a middleware with options (args)
    .UseTimer(new TimerHttpMiddlewareOptions
      {
          WarnThreshold = TimeSpan.Zero
      }) // register a middleware using extension method

As a best practice, it's best to provide an extension method for usage such as UseTimer especially when it has any arguments (options), as it won't be convenient to use.

Request/Response items

When using middleware additional data can be added to the request/response via the .Items of request/response, in order to share state across middleware for the request or to extend response.

The timer middleware example is making use of it.

// set item
response.SetTimeTaken(elapsed);

// or similarly without extension method
response.Items.Add("TIME_TAKEN", value)

// get item
TimeSpan timeTaken = response.GetTimeTaken();

// or similarly without extension method
TimeSpan timeTaken = (TimeSpan)response.Items["TIME_TAKEN"];

Extending

One of the key features is the ability to extend its own APIs easily. In fact, several functions of the library itself are extensions, by using extension methods.

Extending Request Builder

An example of how can the request builder be extended.

public static class FluentHttpRequestBuilderExtensions
{
    public static FluentHttpRequestBuilder WithBearerAuthentication(this FluentHttpRequestBuilder builder, string token)
    {
        if (string.IsNullOrEmpty(token)) throw new ArgumentNullException(nameof(token));
        builder.WithHeader("Authorization", $"Bearer {token}");
        return builder;
    }
}

Extending Request Builder/Client Builder headers

In order to extend headers for both FluentHttpClientBuilder and FluentHttpRequestBuilder, the best approach would be to extend on IFluentHttpHeaderBuilder<T>, this way it will be available for both. See example below.

public static class FluentHttpHeaderBuilderExtensions
{
  public static T WithBearerAuthentication<T>(this IFluentHttpHeaderBuilder<T> builder, string token)
  {
    if (string.IsNullOrEmpty(token)) throw new ArgumentNullException(nameof(token));
    builder.WithHeader(HeaderTypes.Authorization, $"{AuthSchemeTypes.Bearer} {token}");
    return (T)builder;
  }

Extending Request/Response items

In order to extend Items for both FluentHttpRequest and FluentHttpResponse, its best to extend IFluentHttpMessageState. This way it will be available for both. See example below.

public static IDictionary<string, string> GetErrorCodeMappings(this IFluentHttpMessageState message)
{
  if (message.Items.TryGetValue(ErrorCodeMappingKey, out var value))
    return (IDictionary<string, string>)value;
  return null;
}

Recipes

File upload

var multiForm = new MultipartFormDataContent
{
  { "hero", "Jaina" }
};
multiForm.AddFile("file", "./animal-mustache.jpg");

var response = await httpClient.CreateRequest("/api/sample/upload")
  .AsPost()
  .WithBodyContent(multiForm)
  .ReturnAsResponse<MyResult>();

Simple Single file HttpClient

Even though in general we do not suggest (unless for small HttpClients) at times its useful to create a simple quick way http client.

public class SelfInfoHttpClient
{
  private readonly IFluentHttpClient _httpClient;

  public SelfInfoHttpClient(
    IFluentHttpClientFactory httpClientFactory
  )
  {
    _httpClient = httpClientFactory.CreateBuilder("localhost")
      .WithBaseUrl($"http://localhost:5500}")
      .Build();
  }

  public Task<FluentHttpResponse> GetInfo()
    => _httpClient.CreateRequest("info")
      .AsGet()
      .ReturnAsResponse();
}

Testing/Mocking

In order to test HTTP requests, the library itself doesn't offer anything out of the box. However, we've been using RichardSzalay.MockHttp, which we recommend.

Test example with RichardSzalay.MockHttp

[Fact]
public async void ShouldReturnContent()
{
    // build services
    var servicesProvider = new ServiceCollection()
      .AddFluentlyHttpClient()
      .AddLogging()
      .BuildServiceProvider();
    var fluentHttpClientFactory = servicesProvider.GetService<IFluentHttpClientFactory>();

    // define mocks
    var mockHttp = new MockHttpMessageHandler();
    mockHttp.When("https://sketch7.com/api/heroes/azmodan")
      .Respond("application/json", "{ 'name': 'Azmodan' }");

    var httpClient = fluentHttpClientFactory.CreateBuilder("platform")
      .WithBaseUrl("https://sketch7.com")
      .AddMiddleware<TimerHttpMiddleware>()
      .WithMessageHandler(mockHttp) // set message handler to mock
      .Build();

    var response = await httpClient.CreateRequest("/api/heroes/azmodan")
      .ReturnAsResponse<Hero>();

    Assert.NotNull(response.Data);
    Assert.Equal("Azmodan", response.Data.Name);
    Assert.NotEqual(TimeSpan.Zero, response.GetTimeTaken());
}

Contributing

Setup Machine for Development

Install/setup the following:

  • NodeJS v8+
  • Visual Studio Code or similar code editor
  • Git + SourceTree, SmartGit or similar (optional)

Commands

# run tests
npm test

# bump version
npm version minor --no-git-tag # major | minor | patch | prerelease

# nuget pack (only)
npm run pack

# nuget publish dev (pack + publish + clean)
npm run publish:dev

fluentlyhttpclient's People

Contributors

claylaut avatar jonathansant avatar krt-c avatar pfrendo avatar stephenlautier avatar tiasmt 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

fluentlyhttpclient's Issues

WithFormParam?

I don't see any support for form parameters, and such. I see support for query parameters though. Am I missing that somewhere? Other than that, I love the builder interface.

Can I use this repo to download many small http files or strings in parallel

I am writing to inquire about the suitability of your GitHub repository for a specific use case.

I am interested in using your repository to download multiple small files concurrently. The files I need to download are relatively small, ranging from 2KB to 5.6KB. I anticipate having up to 50 URLs, though typically no more than 15. The frequency of downloads varies throughout the day, with some periods having only 1 or 2 URLs, and occasionally none at all.

The URLs resemble the following examples:
http://worldtimeapi.org/Europe/London
http://worldtimeapi.org/Europe/Berlin ......

What I am trying to do is to send a group of http requests to different URLs, but the base URLs are the same, just like http://worldtimeapi.org/Europe/; but the last part of the URLs are different, like: London; Berlin.
I want to send all http requests and get responses in parallel, and process all responses within a short time, less than 1 second. So I need some multiple http download solutions. But my task in not like Free Download Manager, which will download a big file with chunks. Since all the files or json string from each URL is rather small, less than 5.6KB, so better to download each one in only one shot.
But the solution has to fix the http client for concurrency management, and other related issues.
These URLs represent endpoints that provide JSON data, and I need to download them quickly and efficiently, I also have to use Newtonsoft.json to parse the json format string get the necessary information. I plan to implement this functionality in C# using .NET 8.0 and Visual Studio 2022 on Windows 10.

Could you kindly confirm if your repository supports this use case? Additionally, if it does, I would greatly appreciate some general instructions on how to effectively leverage your repository for this purpose.

Thank you for your time and assistance. I look forward to your guidance.

How do I configure Proxy?

Hi, I need to configure a proxy on the request. But I can't find any information of how do I do that.
Can you Help me?
How can I set my Proxy configuration in FluentlyHttpClient.

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.