Skip to content

Commit

Permalink
Merge pull request #774 from tmenier/dev
Browse files Browse the repository at this point in the history
Flurl.Http 4.0-pre6
  • Loading branch information
tmenier authored Nov 3, 2023
2 parents b03f98e + 0759fbc commit b2a7b06
Show file tree
Hide file tree
Showing 22 changed files with 696 additions and 587 deletions.
12 changes: 0 additions & 12 deletions Flurl.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{86A5ACB4-F
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Test", "test\Flurl.Test\Flurl.Test.csproj", "{DF68EB0E-9566-4577-B709-291520383F8D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{B6BF9238-4541-4E1F-955E-C95F1C2A1F46}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
Build\build.cmd = Build\build.cmd
Build\build.sh = Build\build.sh
Build\Flurl.netstandard.sln = Build\Flurl.netstandard.sln
Build\test.cmd = Build\test.cmd
Build\test.coverage.cmd = Build\test.coverage.cmd
Build\test.coverage.sh = Build\test.coverage.sh
Build\test.sh = Build\test.sh
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.CodeGen", "src\Flurl.CodeGen\Flurl.CodeGen.csproj", "{BE943E04-705F-42B1-BF95-A0642D9CA51D}"
EndProject
Global
Expand Down
110 changes: 51 additions & 59 deletions src/Flurl.Http/Configuration/FlurlClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,35 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.Versioning;
using Flurl.Util;

namespace Flurl.Http.Configuration
{
/// <summary>
/// A builder for configuring IFlurlClient instances.
/// </summary>
public interface IFlurlClientBuilder
public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer
{
/// <summary>
/// Configure the IFlurlClient's Settings.
/// </summary>
IFlurlClientBuilder WithSettings(Action<FlurlHttpSettings> configure);

/// <summary>
/// Configure the HttpClient wrapped by this IFlurlClient.
/// </summary>
IFlurlClientBuilder ConfigureHttpClient(Action<HttpClient> configure);

/// <summary>
/// Configure the inner-most HttpMessageHandler associated with this IFlurlClient.
/// Configure the inner-most HttpMessageHandler (an instance of HttpClientHandler) associated with this IFlurlClient.
/// </summary>
#if NETCOREAPP2_1_OR_GREATER
IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configure);
#else
IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configure);

#if NET
/// <summary>
/// Configure a SocketsHttpHandler instead of HttpClientHandler as the inner-most HttpMessageHandler.
/// Note that HttpClientHandler has broader platform support and defers its work to SocketsHttpHandler
/// on supported platforms. It is recommended to explicitly use SocketsHttpHandler ONLY if you
/// need to directly configure its properties that aren't available on HttpClientHandler.
/// </summary>
[UnsupportedOSPlatform("browser")]
IFlurlClientBuilder UseSocketsHttpHandler(Action<SocketsHttpHandler> configure);
#endif

/// <summary>
Expand All @@ -45,36 +49,26 @@ public interface IFlurlClientBuilder
/// </summary>
public class FlurlClientBuilder : IFlurlClientBuilder
{
private readonly IFlurlClientFactory _factory;
private IFlurlClientFactory _factory = new DefaultFlurlClientFactory();

private readonly string _baseUrl;
private readonly List<Func<DelegatingHandler>> _addMiddleware = new();
private readonly List<Action<FlurlHttpSettings>> _configSettings = new();
private readonly List<Action<HttpClient>> _configClient = new();
#if NETCOREAPP2_1_OR_GREATER
private readonly HandlerBuilder<SocketsHttpHandler> _handlerBuilder = new();
#else
private readonly HandlerBuilder<HttpClientHandler> _handlerBuilder = new();
#endif
private readonly List<Action<HttpClient>> _clientConfigs = new();
private readonly List<Action<HttpMessageHandler>> _handlerConfigs = new();

/// <summary>
/// Creates a new FlurlClientBuilder.
/// </summary>
public FlurlClientBuilder(string baseUrl = null) : this(new DefaultFlurlClientFactory(), baseUrl) { }
/// <inheritdoc />
public FlurlHttpSettings Settings { get; } = new();

/// <inheritdoc />
public INameValueList<string> Headers { get; } = new NameValueList<string>(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600

/// <summary>
/// Creates a new FlurlClientBuilder.
/// </summary>
internal FlurlClientBuilder(IFlurlClientFactory factory, string baseUrl) {
_factory = factory;
public FlurlClientBuilder(string baseUrl = null) {
_baseUrl = baseUrl;
}

/// <inheritdoc />
public IFlurlClientBuilder WithSettings(Action<FlurlHttpSettings> configure) {
_configSettings.Add(configure);
return this;
}

/// <inheritdoc />
public IFlurlClientBuilder AddMiddleware(Func<DelegatingHandler> create) {
_addMiddleware.Add(create);
Expand All @@ -83,55 +77,53 @@ public IFlurlClientBuilder AddMiddleware(Func<DelegatingHandler> create) {

/// <inheritdoc />
public IFlurlClientBuilder ConfigureHttpClient(Action<HttpClient> configure) {
_configClient.Add(configure);
_clientConfigs.Add(configure);
return this;
}

/// <inheritdoc />
#if NETCOREAPP2_1_OR_GREATER
public IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configure) {
#else
public IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configure) {
#if NET
if (_factory is SocketsHandlerFlurlClientFactory && _handlerConfigs.Any())
throw new FlurlConfigurationException("ConfigureInnerHandler and UseSocketsHttpHandler cannot be used together. The former configures and instance of HttpClientHandler and would be ignored when switching to SocketsHttpHandler.");
#endif
_handlerBuilder.Configs.Add(configure);
_handlerConfigs.Add(h => configure(h as HttpClientHandler));
return this;
}

#if NET
/// <inheritdoc />
public IFlurlClientBuilder UseSocketsHttpHandler(Action<SocketsHttpHandler> configure) {
if (!SocketsHttpHandler.IsSupported)
throw new PlatformNotSupportedException("SocketsHttpHandler is not supported on one or more target platforms.");

if (_factory is DefaultFlurlClientFactory && _handlerConfigs.Any())
throw new FlurlConfigurationException("ConfigureInnerHandler and UseSocketsHttpHandler cannot be used together. The former configures and instance of HttpClientHandler and would be ignored when switching to SocketsHttpHandler.");

if (!(_factory is SocketsHandlerFlurlClientFactory))
_factory = new SocketsHandlerFlurlClientFactory();

_handlerConfigs.Add(h => configure(h as SocketsHttpHandler));
return this;
}
#endif

/// <inheritdoc />
public IFlurlClient Build() {
var outerHandler = _handlerBuilder.Build(_factory);
var outerHandler = _factory.CreateInnerHandler();
foreach (var config in _handlerConfigs)
config(outerHandler);

foreach (var middleware in Enumerable.Reverse(_addMiddleware).Select(create => create())) {
middleware.InnerHandler = outerHandler;
outerHandler = middleware;
}

var httpCli = _factory.CreateHttpClient(outerHandler);
foreach (var config in _configClient)
foreach (var config in _clientConfigs)
config(httpCli);

var flurlCli = new FlurlClient(httpCli, _baseUrl);
foreach (var config in _configSettings)
config(flurlCli.Settings);

return flurlCli;
}

// helper class to keep those compiler switches from getting too messy
private class HandlerBuilder<T> where T : HttpMessageHandler
{
public List<Action<T>> Configs { get; } = new();

public HttpMessageHandler Build(IFlurlClientFactory fac) {
var handler = fac.CreateInnerHandler();
foreach (var config in Configs) {
if (handler is T h)
config(h);
else
throw new Exception($"ConfigureInnerHandler expected an instance of {typeof(T).Name} but received an instance of {handler.GetType().Name}.");
}
return handler;
}
return new FlurlClient(httpCli, _baseUrl, Settings, Headers);
}
}
}
111 changes: 86 additions & 25 deletions src/Flurl.Http/Configuration/FlurlClientCache.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Flurl.Http.Configuration
{
Expand All @@ -9,52 +10,88 @@ namespace Flurl.Http.Configuration
public interface IFlurlClientCache
{
/// <summary>
/// Adds and returns a new IFlurlClient. Call once per client at startup to register and configure a named client.
/// Adds a new IFlurlClient to this cache. Call once per client at startup to register and configure a named client.
/// </summary>
/// <param name="name">Name of the IFlurlClient. Serves as a cache key. Subsequent calls to Get will return this client.</param>
/// <param name="baseUrl">Optional. The base URL associated with the new client.</param>
/// <returns></returns>
/// <returns>A builder to further configure the new client.</returns>
IFlurlClientBuilder Add(string name, string baseUrl = null);

/// <summary>
/// Gets a named IFlurlClient, creating one if it doesn't exist or has been disposed.
/// Gets a preconfigured named IFlurlClient.
/// </summary>
/// <param name="name">The client name.</param>
/// <returns></returns>
/// <returns>The cached IFlurlClient.</returns>
IFlurlClient Get(string name);

/// <summary>
/// Configuration logic that gets executed for every new IFlurlClient added this case. Good place for things like default
/// settings. Executes before client-specific builder logic.
/// Gets a named IFlurlClient, creating and (optionally) configuring one if it doesn't exist or has been disposed.
/// </summary>
IFlurlClientCache ConfigureAll(Action<IFlurlClientBuilder> configure);
/// <param name="name">The client name.</param>
/// <param name="baseUrl">The base URL associated with the new client, if it doesn't exist.</param>
/// <param name="configure">Configure the builder associated with the new client, if it doesn't exist.</param>
/// <returns>The cached IFlurlClient.</returns>
IFlurlClient GetOrAdd(string name, string baseUrl = null, Action<IFlurlClientBuilder> configure = null);

/// <summary>
/// Adds initialization logic that gets executed for every new IFlurlClient added this cache.
/// Good place for things like default settings. Executes before client-specific builder logic.
/// Call at startup (or whenever the cache is first created); clients already cached will NOT have this logic applied.
/// </summary>
/// <returns>This IFlurlCache.</returns>
IFlurlClientCache WithDefaults(Action<IFlurlClientBuilder> configure);

/// <summary>
/// Removes a named client from this cache.
/// </summary>
void Remove(string name);
/// <returns>This IFlurlCache.</returns>
IFlurlClientCache Remove(string name);

/// <summary>
/// Disposes and removes all cached IFlurlClient instances.
/// </summary>
void Clear();
/// <returns>This IFlurlCache.</returns>
IFlurlClientCache Clear();
}

/// <summary>
/// Extension methods on IFlurlClientCache.
/// </summary>
public static class IFlurlClientCacheExtensions
{
/// <summary>
/// Adds a new IFlurlClient to this cache. Call once per client at startup to register and configure a named client.
/// Allows configuring via a nested lambda, rather than returning a builder, so multiple Add calls can be fluently chained.
/// </summary>
/// <param name="cache">This IFlurlCache</param>
/// <param name="name">Name of the IFlurlClient. Serves as a cache key. Subsequent calls to Get will return this client.</param>
/// <param name="baseUrl">The base URL associated with the new client.</param>
/// <param name="configure">Configure the builder associated with the added client.</param>
/// <returns>This IFlurlCache.</returns>
public static IFlurlClientCache Add(this IFlurlClientCache cache, string name, string baseUrl, Action<IFlurlClientBuilder> configure) {
var builder = cache.Add(name, baseUrl);
configure?.Invoke(builder);
return cache;
}
}

/// <summary>
/// Default implementation of IFlurlClientCache.
/// </summary>
public class FlurlClientCache : IFlurlClientCache {
public class FlurlClientCache : IFlurlClientCache
{
private readonly ConcurrentDictionary<string, Lazy<IFlurlClient>> _clients = new();
private readonly IFlurlClientFactory _factory = new DefaultFlurlClientFactory();
private Action<IFlurlClientBuilder> _configureAll;
private readonly List<Action<IFlurlClientBuilder>> _defaultConfigs = new();

/// <inheritdoc />
public IFlurlClientBuilder Add(string name, string baseUrl = null) {
if (_clients.ContainsKey(name))
throw new ArgumentException($"A client named '{name}' was already registered with this factory. AddClient should be called just once per client at startup.");
if (name == null)
throw new ArgumentNullException(nameof(name));

var builder = CreateBuilder(baseUrl);
if (!_clients.TryAdd(name, new Lazy<IFlurlClient>(builder.Build)))
throw new ArgumentException($"A client named '{name}' was already registered. Add should be called just once per client at startup.");

var builder = new FlurlClientBuilder(_factory, baseUrl);
_clients[name] = CreateLazyInstance(builder);
return builder;
}

Expand All @@ -63,32 +100,56 @@ public virtual IFlurlClient Get(string name) {
if (name == null)
throw new ArgumentNullException(nameof(name));

Lazy<IFlurlClient> Create() => CreateLazyInstance(new FlurlClientBuilder(_factory, null));
return _clients.AddOrUpdate(name, _ => Create(), (_, existing) => existing.Value.IsDisposed ? Create() : existing).Value;
if (!_clients.TryGetValue(name, out var cli))
throw new ArgumentException($"A client named '{name}' was not found. Either preconfigure the client using Add (typically at startup), or use GetOrAdd to add/configure one on demand when needed.");

if (cli.Value.IsDisposed)
throw new Exception($"A client named '{name}' was not found but has been disposed and cannot be reused.");

return cli.Value;
}

private Lazy<IFlurlClient> CreateLazyInstance(FlurlClientBuilder builder) {
_configureAll?.Invoke(builder);
return new Lazy<IFlurlClient>(builder.Build);
/// <inheritdoc />
public IFlurlClient GetOrAdd(string name, string baseUrl = null, Action<IFlurlClientBuilder> configure = null) {
if (name == null)
throw new ArgumentNullException(nameof(name));

Lazy<IFlurlClient> Create() {
var builder = CreateBuilder(baseUrl);
configure?.Invoke(builder);
return new Lazy<IFlurlClient>(builder.Build);
}

return _clients.AddOrUpdate(name, _ => Create(), (_, existing) => existing.Value.IsDisposed ? Create() : existing).Value;
}

/// <inheritdoc />
public IFlurlClientCache ConfigureAll(Action<IFlurlClientBuilder> configure) {
_configureAll = configure;
public IFlurlClientCache WithDefaults(Action<IFlurlClientBuilder> configure) {
if (configure != null)
_defaultConfigs.Add(configure);
return this;
}

/// <inheritdoc />
public void Remove(string name) {
public IFlurlClientCache Remove(string name) {
if (_clients.TryRemove(name, out var cli) && cli.IsValueCreated && !cli.Value.IsDisposed)
cli.Value.Dispose();
return this;
}

/// <inheritdoc />
public void Clear() {
public IFlurlClientCache Clear() {
// Remove takes care of disposing too, which is why we don't simply call _clients.Clear
foreach (var key in _clients.Keys)
Remove(key);
return this;
}

private IFlurlClientBuilder CreateBuilder(string baseUrl) {
var builder = new FlurlClientBuilder(baseUrl);
foreach (var config in _defaultConfigs)
config(builder);
return builder;
}
}
}
Loading

0 comments on commit b2a7b06

Please sign in to comment.