Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@

<!-- Common compile parameters -->
<PropertyGroup>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);1701;1702;1705;1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);1701;1702;1705</NoWarn>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<LangVersion>14.0</LangVersion>
<Nullable>enable</Nullable>
Expand Down
5 changes: 3 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.1" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<!-- 3rd party -->
Expand All @@ -26,7 +26,8 @@
<PackageVersion Include="Sketch7.Core" Version="0.2.0" />
<!-- testing -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0" />
Expand Down
107 changes: 39 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.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.
Http Client for .NET with fluent APIs which are intuitive, easy to use and also highly extensible.

**Quick links**

Expand Down Expand Up @@ -35,6 +35,7 @@ Http Client for .NET Standard with fluent APIs which are intuitive, easy to use
| 2.x | .NET Standard 2 | |
| 3.x | .NET Standard 2 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg?branch=3.x)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) |
| 4.x | net8 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg?branch=v4)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) |
| 5.x | net10 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg?branch=v5)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) |

### NuGet
```
Expand Down Expand Up @@ -69,7 +70,7 @@ PM> Install-Package FluentlyHttpClient
- [Usage](#usage-1)
- [Query params](#query-params)
- [Interpolate Url](#interpolate-url)
- [ReturnAsReponse, ReturnAsResponse`<T>` and Return`<T>`](#returnasreponse-returnasresponset-and-returnt)
- [ReturnAsResponse, ReturnAsResponse`<T>` and Return`<T>`](#returnasresponse-returnasresponset-and-returnt)
- [GraphQL](#graphql)
- [Middleware](#middleware)
- [Middleware options](#middleware-options)
Expand All @@ -84,35 +85,28 @@ PM> Install-Package FluentlyHttpClient
- [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();
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFluentlyHttpClient();
var app = builder.Build();
```

Configure an Http client using the Http Factory (you need at least one).
Configure an HTTP client using the HTTP client 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<LoggerHttpMiddleware>()
.Register(); // register client builder to factory
}
var fluentHttpClientFactory = app.Services.GetRequiredService<IFluentHttpClientFactory>();
fluentHttpClientFactory.CreateBuilder(identifier: "platform") // keep a note of the identifier, it's needed later
.WithBaseUrl("https://sketch7.com") // required
.WithHeader("user-agent", "my-app")
.WithTimeout(5)
.UseMiddleware<LoggerHttpMiddleware>()
.Register(); // register client builder to factory
```

### Basic usage
Expand All @@ -124,11 +118,11 @@ Simple API (non-fluent) is good for simple requests as it has a clean, minimal A
// 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 GET + deserialize result (non-fluent 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
// HTTP POST + deserialize result (non-fluent API)
Hero? hero = await httpClient.Post<Hero>("/api/heroes/azmodan", new
{
Title = "Lord of Sin"
});
Expand All @@ -147,7 +141,7 @@ FluentHttpResponse<Hero> response =
.ReturnAsResponse<Hero>(); // return with response

// HTTP POST + return response and deserialize result (fluent API)
Hero hero = await httpClient.CreateRequest("/api/heroes/azmodan")
Hero? hero = await httpClient.CreateRequest("/api/heroes/azmodan")
.AsPost()
.WithBody(new
{
Expand All @@ -163,11 +157,11 @@ Http client builder is used to configure http clients in a fluent way.

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

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

#### Register multiple + share
Expand Down Expand Up @@ -237,7 +231,8 @@ httpClientBuilder.ConfigureFormatters(opts =>
opts.Formatters.Add(new CustomFormatter());
});

httpClientBuilder.WithVersion(HttpVersion.Version30) // specify to use http3 (defaults: http2)
// http version - configure per-request via request builder defaults
httpClientBuilder.WithRequestBuilderDefaults(builder => builder.WithVersion(HttpVersion.Version30)); // specify http3 per-request (default: http2)
```

#### Re-using Http Client from Factory
Expand All @@ -259,7 +254,7 @@ Request builder is used to build http requests in a fluent way.
#### Usage

```cs
LoginResponse loginResponse =
LoginResponse? loginResponse =
await fluentHttpClient.CreateRequest("/api/auth/login")
.AsPost() // set as HTTP Post
.WithBody(new
Expand Down Expand Up @@ -292,7 +287,7 @@ requestBuilder.WithUri("{Language}/heroes/{Hero}", new
}); // => /en/heroes/azmodan
```

#### ReturnAsReponse, ReturnAsResponse`<T>` and Return`<T>`
#### ReturnAsResponse, ReturnAsResponse`<T>` and Return`<T>`

```cs
// send and returns HTTP response
Expand All @@ -301,8 +296,8 @@ 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>();
// send and returns deserialized result directly
Hero? hero = requestBuilder.Return<Hero>();
```


Expand All @@ -317,7 +312,7 @@ httpClientBuilder.WithRequestBuilderDefaults(requestBuilder => requestBuilder.Wi
FluentHttpResponse<Hero> response =
await fluentHttpClient.CreateGqlRequest("{ hero {name, title } }")
.ReturnAsGqlResponse<Hero>();
// => response.Data.Title
// => response.Data?.Title
```


Expand All @@ -334,9 +329,9 @@ These are provided out of the box:
| Timer | Determine how long (timespan) requests takes. |
| Logger | Log request/response. |

Two important points to keep in mind:
Three 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`,
- 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)*.
Expand Down Expand Up @@ -388,7 +383,7 @@ namespace FluentlyHttpClient
// FluentHttpClientBuilder extension methods - add
public static class FluentlyHttpMiddlwareExtensions
{
public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions options = null)
public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions? options = null)
=> builder.UseMiddleware<TimerHttpMiddleware>(options ?? new TimerHttpMiddlewareOptions());
}
}
Expand All @@ -398,7 +393,7 @@ 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.
Options to middleware can be passed via an argument. Note it has to be the third argument within the constructor (after `next` and `context`).

```cs
public TimerHttpMiddleware(
Expand Down Expand Up @@ -482,13 +477,14 @@ public static class FluentHttpHeaderBuilderExtensions
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<string, string> GetErrorCodeMappings(this IFluentHttpMessageState message)
public static IDictionary<string, string>? GetErrorCodeMappings(this IFluentHttpMessageState message)
{
if (message.Items.TryGetValue(ErrorCodeMappingKey, out var value))
return (IDictionary<string, string>)value;
Expand Down Expand Up @@ -526,7 +522,7 @@ public class SelfInfoHttpClient
)
{
_httpClient = httpClientFactory.CreateBuilder("localhost")
.WithBaseUrl($"http://localhost:5500}")
.WithBaseUrl("http://localhost:5500")
.Build();
}

Expand All @@ -545,14 +541,14 @@ However, we've been using [RichardSzalay.MockHttp](https://github.com/richardsza

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

// define mocks
var mockHttp = new MockHttpMessageHandler();
Expand All @@ -561,7 +557,7 @@ public async void ShouldReturnContent()

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

Expand All @@ -573,28 +569,3 @@ public async void ShouldReturnContent()
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
```
24 changes: 12 additions & 12 deletions benchmark/Benchmarking.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public void Setup()
{
var mockHttp = new MockHttpMessageHandler();
mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/json")
.Respond("application/json", request => request.Content.ReadAsStreamAsync().Result);
.Respond("application/json", request => request.Content!.ReadAsStreamAsync().Result);

mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/msgpack")
//.Respond("application/x-msgpack", "��Key�valeera�Name�Valeera�Title�Shadow of the Uncrowned")
.Respond("application/x-msgpack", request => request.Content.ReadAsStreamAsync().Result);
//.Respond("application/x-msgpack", "\x99\xa3Key\xa6valeera...")
.Respond("application/x-msgpack", request => request.Content!.ReadAsStreamAsync().Result);

var fluentHttpClientFactory = BuildContainer()
.GetRequiredService<IFluentHttpClientFactory>();
Expand Down Expand Up @@ -79,33 +79,33 @@ public void Setup()
;

Console.WriteLine($"Setup Complete");
Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient.DefaultFormatter.GetType().Name}");
Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient.DefaultFormatter.GetType().Name}");
Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient.DefaultFormatter.GetType().Name}");
Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient!.DefaultFormatter?.GetType().Name}");
Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient!.DefaultFormatter?.GetType().Name}");
Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient!.DefaultFormatter?.GetType().Name}");
}

[Benchmark]
public Task<Hero> PostAsJson()
public Task<Hero?> PostAsJson()
{
return _jsonHttpClient.CreateRequest("/api/json")
return _jsonHttpClient!.CreateRequest("/api/json")
.AsPost()
.WithBody(_request)
.Return<Hero>();
}

[Benchmark]
public Task<Hero> PostAsMessagePack()
public Task<Hero?> PostAsMessagePack()
{
return _messagePackHttpClient.CreateRequest("/api/msgpack")
return _messagePackHttpClient!.CreateRequest("/api/msgpack")
.AsPost()
.WithBody(_request)
.Return<Hero>();
}

[Benchmark]
public Task<Hero> PostAsSystemTextJson()
public Task<Hero?> PostAsSystemTextJson()
{
return _systemTextJsonHttpClient.CreateRequest("/api/json")
return _systemTextJsonHttpClient!.CreateRequest("/api/json")
.AsPost()
.WithBody(_request)
.Return<Hero>();
Expand Down
1 change: 1 addition & 0 deletions benchmark/FluentlyHttpClient.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sketch7/fluently-http-client",
"version": "4.1.2",
"version": "5.0.0",
"scripts": {
"pack": "bash ./tools/pack.sh",
"prepublish:dev": "npm run pack",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Humanizer.Core" />
</ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions samples/FluentlyHttpClient.Sample.Api/Heroes/Hero.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace FluentlyHttpClient.Sample.Api.Heroes;
public record Hero
{
[Required]
public string Key { get; set; }
public required string Key { get; init; }

[Required]
public string Name { get; set; }
public string Title { get; set; }
public required string Name { get; init; }
public string? Title { get; init; }
}
Loading
Loading