Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/sketch7/FluentlyHttpClient
Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible.
https://github.com/sketch7/FluentlyHttpClient
csharp dotnet fluent-api fluentapi http-client httpclient middleware netstandard rest rest-client
Last synced: 1 day ago
JSON representation
Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible.
- Host: GitHub
- URL: https://github.com/sketch7/FluentlyHttpClient
- Owner: sketch7
- License: mit
- Created: 2017-06-23T22:40:18.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2024-10-11T13:32:38.000Z (about 1 month ago)
- Last Synced: 2024-10-11T13:45:46.045Z (about 1 month ago)
- Topics: csharp, dotnet, fluent-api, fluentapi, http-client, httpclient, middleware, netstandard, rest, rest-client
- Language: C#
- Homepage:
- Size: 541 KB
- Stars: 114
- Watchers: 3
- Forks: 17
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Codeowners: CODEOWNERS
Awesome Lists containing this project
README
[projectUri]: https://github.com/sketch7/FluentlyHttpClient
[projectGit]: https://github.com/sketch7/FluentlyHttpClient.git
[changeLog]: ./CHANGELOG.md# Fluently Http Client
[![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/dotnet-publish.yml/badge.svg)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/dotnet-publish.yml)
[![NuGet version](https://badge.fury.io/nu/fluentlyhttpclient.svg)](https://badge.fury.io/nu/fluentlyhttpclient)Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible.
**Quick links**
[Change logs][changeLog] | [Project Repository][projectUri]
## 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
## dotnet support
| Version | .NET | Status |
| ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.x | .NET Standard 1.4+ | |
| 2.x | .NET Standard 2 | |
| 3.x | .NET Standard 2 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/dotnet-publish.yml/badge.svg?branch=3.x)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/dotnet-publish.yml) |
| 4.x | net8 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/dotnet-publish.yml/badge.svg?branch=4.x)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/dotnet-publish.yml) |### NuGet
```
PM> Install-Package FluentlyHttpClient
```### csproj
```xml
```
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [dotnet support](#dotnet-support)
- [NuGet](#nuget)
- [csproj](#csproj)
- [Usage](#usage)
- [Configure](#configure)
- [Basic usage](#basic-usage)
- [Simple API](#simple-api)
- [Fluent Request API](#fluent-request-api)
- [Fluent Http Client Builder](#fluent-http-client-builder)
- [Register to Factory](#register-to-factory)
- [Register multiple + share](#register-multiple--share)
- [Create Http Client from Client](#create-http-client-from-client)
- [Configure defaults for Http Clients](#configure-defaults-for-http-clients)
- [Http Client Builder extra goodies](#http-client-builder-extra-goodies)
- [Re-using Http Client from Factory](#re-using-http-client-from-factory)
- [Request Builder](#request-builder)
- [Usage](#usage-1)
- [Query params](#query-params)
- [Interpolate Url](#interpolate-url)
- [ReturnAsReponse, ReturnAsResponse`` and Return``](#returnasreponse-returnasresponset-and-returnt)
- [GraphQL](#graphql)
- [Middleware](#middleware)
- [Middleware options](#middleware-options)
- [Use a middleware](#use-a-middleware)
- [Request/Response items](#requestresponse-items)
- [Extending](#extending)
- [Extending Request Builder](#extending-request-builder)
- [Extending Request Builder/Client Builder headers](#extending-request-builderclient-builder-headers)
- [Extending Request/Response items](#extending-requestresponse-items)
- [Recipes](#recipes)
- [File upload](#file-upload)
- [Simple Single file HttpClient](#simple-single-file-httpclient)
- [Testing/Mocking](#testingmocking)
- [Test example with RichardSzalay.MockHttp](#test-example-with-richardszalaymockhttp)
- [Contributing](#contributing)
- [Setup Machine for Development](#setup-machine-for-development)
- [Commands](#commands)## Usage
### Configure
Add services via `.AddFluentlyHttpClient()`.```cs
// 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).
```cs
// 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()
.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.```cs
// inject factory and get client
var httpClient = fluentHttpClientFactory.Get(identifier: "platform");// HTTP GET + deserialize result (non-fleunt API)
Hero hero = await httpClient.Get("/api/heroes/azmodan");// HTTP POST + deserialize result (non-fleunt API)
Hero hero = await httpClient.Post("/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.```cs
// inject factory and get client
var httpClient = fluentHttpClientFactory.Get(identifier: "platform");// HTTP GET + return response and deserialize result (fluent API)
FluentHttpResponse response =
await httpClient.CreateRequest("/api/heroes/azmodan")
.ReturnAsResponse(); // 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(); // return deserialized result directly
```### Fluent Http Client Builder
Http client builder is used to configure http clients in a fluent way.#### Register to Factory
```cs
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:```cs
fluentHttpClientFactory.CreateBuilder("platform")
// shared
.WithHeader("user-agent", "slabs-testify")
.WithTimeout(5)
.UseTimer()
.UseMiddleware()// platform
.WithBaseUrl("https://platform.com")
.Register()// big-data - reuse all above and replace the below
.WithIdentifier("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.```cs
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.```cs
fluentHttpClientFactory.ConfigureDefaults(builder => builder
.WithUserAgent("sketch7")
.WithTimeout(5)
);// or
services.AddFluentlyHttpClient(builder => builder
.WithUserAgent("sketch7")
)
```#### Http Client Builder extra goodies
```cs
// 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(); // use messagepack
opts.Default = opts.Formatters.SystemTextJsonFormatter(); // use system.text.json
opts.Formatters.Add(new CustomFormatter());
});httpClientBuilder.WithVersion(HttpVersion.Version30) // specify to use http3 (defaults: http2)
```#### 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.```cs
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
```cs
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(); // send, deserialize result and return result directly.
```#### Query params
```cs
requestBuilder.WithQueryParams(new
{
Take = 5,
Roles = new List { "warrior", "assassin" },
}, opts => {
opts.CollectionMode = QueryStringCollectionMode.CommaSeparated;
opts.KeyFormatter = key => key.ToLower();
}); // => /url?roles=warrior,assassin&take=5
```#### Interpolate Url
```cs
requestBuilder.WithUri("{Language}/heroes/{Hero}", new
{
Language = "en",
Hero = "azmodan"
}); // => /en/heroes/azmodan
```#### ReturnAsReponse, ReturnAsResponse`` and Return``
```cs
// send and returns HTTP response
FluentHttpResponse response = requestBuilder.ReturnAsResponse();// send and returns HTTP response + deserialize and return result via `.Data`
FluentHttpResponse response = requestBuilder.ReturnAsResponse();// send and returns derserialized result directly
Hero hero = requestBuilder.Return();
```### GraphQL
FluentlyHttpClient :heart: GraphQL. First class support for GraphQL to be able to create request/response even simpler.```cs
// 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 response =
await fluentHttpClient.CreateGqlRequest("{ hero {name, title } }")
.ReturnAsGqlResponse();
// => 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)*.
```cs
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 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(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.```cs
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
```cs
fluentHttpClientFactory.CreateBuilder("platform")
.UseMiddleware() // register a middleware (without args)
.UseMiddleware(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.
```cs
// 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.```cs
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`, this way it will be available for both. See example below.```cs
public static class FluentHttpHeaderBuilderExtensions
{
public static T WithBearerAuthentication(this IFluentHttpHeaderBuilder 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.```cs
public static IDictionary GetErrorCodeMappings(this IFluentHttpMessageState message)
{
if (message.Items.TryGetValue(ErrorCodeMappingKey, out var value))
return (IDictionary)value;
return null;
}
```### Recipes
#### File upload
```cs
var multiForm = new MultipartFormDataContent
{
{ "hero", "Jaina" }
};
multiForm.AddFile("file", "./animal-mustache.jpg");var response = await httpClient.CreateRequest("/api/sample/upload")
.AsPost()
.WithBodyContent(multiForm)
.ReturnAsResponse();
```#### 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.```cs
public class SelfInfoHttpClient
{
private readonly IFluentHttpClient _httpClient;public SelfInfoHttpClient(
IFluentHttpClientFactory httpClientFactory
)
{
_httpClient = httpClientFactory.CreateBuilder("localhost")
.WithBaseUrl($"http://localhost:5500}")
.Build();
}public Task 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](https://github.com/richardszalay/mockhttp), which we recommend.#### Test example with RichardSzalay.MockHttp
```cs
[Fact]
public async void ShouldReturnContent()
{
// build services
var servicesProvider = new ServiceCollection()
.AddFluentlyHttpClient()
.AddLogging()
.BuildServiceProvider();
var fluentHttpClientFactory = servicesProvider.GetService();// 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()
.WithMessageHandler(mockHttp) // set message handler to mock
.Build();var response = await httpClient.CreateRequest("/api/heroes/azmodan")
.ReturnAsResponse();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
```bash
# 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
```