Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http client builder): auto register clients + configurable defaults with AddFluentlyHttpClient #90

Merged
merged 3 commits into from
Aug 28, 2024
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
### Features

- **http client factory:** expose `Count` to know how many clients are registered
- **http client builder:** auto register client into factory - can be omitted via `WithAutoRegisterFactory` (or `.Build(skipAutoRegister: true)`)
- **service collection:** configurable defaults on `services.AddFluentlyHttpClient`

## [4.0.0](https://github.com/sketch7/FluentlyHttpClient/compare/3.9.6...4.0.0) (2024-07-24)

Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ fluentHttpClientFactory.CreateBuilder("platform")
.Register()

// big-data - reuse all above and replace the below
.Withdentifier("big-data")
.WithIdentifier("big-data")
.WithBaseUrl("https://api.big-data.com")
.Register();
```
Expand All @@ -208,10 +208,15 @@ Its also possible to configure builder defaults for all http clients via `Config
See example below.

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

// or
services.AddFluentlyHttpClient(builder => builder
.WithUserAgent("sketch7")
)
```

#### Http Client Builder extra goodies
Expand Down
27 changes: 23 additions & 4 deletions src/FluentlyHttpClient/FluentHttpClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class FluentHttpClientBuilder : IFluentHttpHeaderBuilder<FluentHttpClient
private HttpMessageHandler? _httpMessageHandler;
private readonly FormatterOptions _formatterOptions = new();
private bool _useBaseUrlTrailingSlash = true;
private bool _autoRegisterFactory = true;

/// <summary>
/// Initializes a new instance.
Expand All @@ -38,6 +39,17 @@ FluentHttpMiddlewareBuilder middlewareBuilder
_middlewareBuilder = middlewareBuilder;
}

/// <summary>
/// Determine whether to auto register client in factory. Must have unique identifier.
/// </summary>
/// <param name="autoRegister"></param>
/// <returns></returns>
public FluentHttpClientBuilder WithAutoRegisterFactory(bool autoRegister = true)
{
_autoRegisterFactory = autoRegister;
return this;
}

/// <summary>
/// Set base url for each request.
/// </summary>
Expand Down Expand Up @@ -216,30 +228,36 @@ public FluentHttpClientOptions BuildOptions()
Formatters = _formatterOptions.Formatters,
DefaultFormatter = _formatterOptions.Default,
UseBaseUrlTrailingSlash = _useBaseUrlTrailingSlash,
AutoRegisterFactory = _autoRegisterFactory
};
}

/// <summary>
/// Builds a new HTTP client (with default <see cref="FluentHttpClient"/> implementation).
/// </summary>
/// <param name="options"></param>
public IFluentHttpClient Build(FluentHttpClientOptions? options = null)
=> Build<FluentHttpClient>(options);
/// <param name="skipAutoRegister">Determine whether to skip auto register, even if on the builder is configured to be true.</param>
public IFluentHttpClient Build(FluentHttpClientOptions? options = null, bool? skipAutoRegister = null)
=> Build<FluentHttpClient>(options, skipAutoRegister);

/// <summary>
/// Build a new HTTP client.
/// </summary>
/// <typeparam name="THttpClient">HttpClient type</typeparam>
/// <param name="options"></param>
public IFluentHttpClient Build<THttpClient>(FluentHttpClientOptions? options = null)
/// <param name="skipAutoRegister">Determine whether to skip auto register, even if on the builder is configured to be true.</param>
public IFluentHttpClient Build<THttpClient>(FluentHttpClientOptions? options = null, bool? skipAutoRegister = null)
where THttpClient : IFluentHttpClient
{
options ??= BuildOptions();

if (string.IsNullOrEmpty(options.Identifier))
throw ClientBuilderValidationException.FieldNotSpecified(nameof(options.Identifier));

return ActivatorUtilities.CreateInstance<THttpClient>(_serviceProvider, options, _fluentHttpClientFactory);
var client = ActivatorUtilities.CreateInstance<THttpClient>(_serviceProvider, options, _fluentHttpClientFactory);
if (_autoRegisterFactory && skipAutoRegister is not true)
_fluentHttpClientFactory.Add(client);
return client;
}

/// <summary>
Expand Down Expand Up @@ -272,6 +290,7 @@ public FluentHttpClientBuilder FromOptions(FluentHttpClientOptions options)
_formatterOptions.Formatters.Clear();
_formatterOptions.Formatters.AddRange(formatters);
_formatterOptions.Default = options.DefaultFormatter;
_autoRegisterFactory = options.AutoRegisterFactory;

return this;
}
Expand Down
20 changes: 14 additions & 6 deletions src/FluentlyHttpClient/FluentHttpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace FluentlyHttpClient;

public record FluentHttpClientFactoryOptions(
Action<FluentHttpClientBuilder>? ConfigureDefaults
);
/// <summary>
/// HTTP client factory which contains registered HTTP clients and able to get existing or creating new ones.
/// </summary>
Expand Down Expand Up @@ -76,9 +79,15 @@ public class FluentHttpClientFactory : IFluentHttpClientFactory
/// Initializes a new instance of <see cref="FluentHttpClientFactory"/>.
/// </summary>
/// <param name="serviceProvider"></param>
public FluentHttpClientFactory(IServiceProvider serviceProvider)
/// <param name="options"></param>
public FluentHttpClientFactory(
IServiceProvider serviceProvider,
FluentHttpClientFactoryOptions options
)
{
_serviceProvider = serviceProvider;
if (options.ConfigureDefaults != null)
ConfigureDefaults(options.ConfigureDefaults);
}

/// <inheritdoc />
Expand Down Expand Up @@ -112,7 +121,7 @@ public IFluentHttpClient Get(string identifier)
/// <inheritdoc />
public IFluentHttpClient Add(IFluentHttpClient client)
{
if (client == null) throw new ArgumentNullException(nameof(client));
ArgumentNullException.ThrowIfNull(client);

if (Has(client.Identifier))
throw new ClientBuilderValidationException($"FluentHttpClient '{client.Identifier}' is already registered.");
Expand All @@ -123,19 +132,18 @@ public IFluentHttpClient Add(IFluentHttpClient client)
/// <inheritdoc />
public IFluentHttpClient Add(FluentHttpClientBuilder clientBuilder)
{
if (clientBuilder == null) throw new ArgumentNullException(nameof(clientBuilder));
ArgumentNullException.ThrowIfNull(clientBuilder);

var client = clientBuilder.Build();
var client = clientBuilder.Build(skipAutoRegister: true);
return Add(client);
}

/// <inheritdoc />
public IFluentHttpClientFactory Remove(string identifier)
{
if (!_clientsMap.TryGetValue(identifier, out var client))
if (!_clientsMap.Remove(identifier, out var client))
return this;

_clientsMap.Remove(identifier);
client.Dispose();
return this;
}
Expand Down
5 changes: 5 additions & 0 deletions src/FluentlyHttpClient/FluentHttpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public class FluentHttpClientOptions
/// Gets or sets the default formatter to be used for content negotiation body format. e.g. JSON, XML, etc...
/// </summary>
public MediaTypeFormatter? DefaultFormatter { get; set; }

/// <summary>
/// Determine whether to auto register in factory.
/// </summary>
public bool AutoRegisterFactory { get; set; }
}

/// <summary>
Expand Down
11 changes: 8 additions & 3 deletions src/FluentlyHttpClient/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using FluentlyHttpClient;
using FluentlyHttpClient;
using FluentlyHttpClient.Caching;
using FluentlyHttpClient.Middleware;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand All @@ -15,16 +15,21 @@ public static class FluentlyHttpClientServiceCollectionExtensions
/// Adds fluently HTTP client services to the specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services"></param>
/// <param name="configureDefaults">Configure defaults.</param>
/// <returns>Returns service collection for chaining.</returns>
public static IServiceCollection AddFluentlyHttpClient(this IServiceCollection services)
public static IServiceCollection AddFluentlyHttpClient(
this IServiceCollection services,
Action<FluentHttpClientBuilder>? configureDefaults = null
)
{
if (services == null) throw new ArgumentNullException(nameof(services));
ArgumentNullException.ThrowIfNull(services);

services.TryAddSingleton<IHttpResponseSerializer, HttpResponseSerializer>();
services.TryAddSingleton<IFluentHttpClientFactory, FluentHttpClientFactory>();
services.TryAddTransient<FluentHttpMiddlewareBuilder>();
services.AddMemoryCache();
services.TryAddSingleton<IResponseCacheService, MemoryResponseCacheService>();
services.TryAddSingleton(new FluentHttpClientFactoryOptions(configureDefaults));

services.AddHttpClient();

Expand Down
16 changes: 16 additions & 0 deletions test/FluentHttpClientFactoryTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http.Formatting;
using static FluentlyHttpClient.Test.ServiceTestUtil;

Expand All @@ -18,6 +19,20 @@ public void ShouldAllowEmptyBaseUrl()

public class ClientFactory_WithRequestBuilderDefaults
{
[Fact]
public void AddFluentlyHttpClient_Defaults_ShouldBeSet()
{
var f = new ServiceCollection()
.AddFluentlyHttpClient(defaults => defaults
.WithTimeout(200)
.WithUserAgent("default-config")
).BuildServiceProvider()
.GetRequiredService<IFluentHttpClientFactory>();

var client = f.CreateBuilder("sketch7").Build();
Assert.Equal("default-config", client.Headers.UserAgent.ToString());
}

[Fact]
public void ShouldHaveWithCustomDefaultsSet()
{
Expand Down Expand Up @@ -142,6 +157,7 @@ public void ShouldSetDefaultFormatter()
public void SetDefaultFormatterMany_ShouldBeSetCorrectly()
{
var clientBuilder = GetNewClientFactory()
.ConfigureDefaults(x => x.WithAutoRegisterFactory(false))
.CreateBuilder("abc")
.WithBaseUrl("http://abc.com")
;
Expand Down
9 changes: 6 additions & 3 deletions test/FluentHttpClientTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Sketch7.MessagePack.MediaTypeFormatter;
using static FluentlyHttpClient.Test.ServiceTestUtil;

Expand Down Expand Up @@ -57,7 +58,9 @@ public async void Post_ShouldReturnContent()
[Fact]
public void CreateClient_ShouldInheritOptions()
{
var clientBuilder = GetNewClientFactory().CreateBuilder("sketch7")
var serviceProvider = CreateContainer().BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IFluentHttpClientFactory>();
var clientBuilder = httpClientFactory.CreateBuilder("sketch7")
.WithBaseUrl("https://sketch7.com")
.WithHeader("locale", "en-GB")
.UseTimer()
Expand All @@ -83,14 +86,12 @@ public void CreateClient_ShouldInheritOptions()
var httpClientRequest = httpClient.CreateRequest();
var subClientRequest = subClient.CreateRequest();


var httpClientLocale = httpClient.Headers.GetValues("locale").FirstOrDefault();
var subClientLocale = subClient.Headers.GetValues("locale").FirstOrDefault();

httpClient.Headers.TryGetValues("country", out var countryValues);
var subClientCountry = subClient.Headers.GetValues("country").FirstOrDefault();


Assert.Equal("sketch7", httpClient.Identifier);
Assert.Equal("sketch7.subclient", subClient.Identifier);
Assert.Equal("en-GB", httpClientLocale);
Expand All @@ -104,6 +105,8 @@ public void CreateClient_ShouldInheritOptions()
Assert.Equal("reward", subClientRequest.Items["context"]);
Assert.Equal(httpClient.Formatters.Count, subClient.Formatters.Count);
// todo: check middleware count?

Assert.Equal(2, httpClientFactory.Count);
}

[Fact]
Expand Down
Loading