Skip to content

Commit

Permalink
Merge pull request #771 from tmenier/dev
Browse files Browse the repository at this point in the history
Flurl.Http 4.0-pre5
  • Loading branch information
tmenier authored Oct 22, 2023
2 parents f022836 + 8dbd2b9 commit b03f98e
Show file tree
Hide file tree
Showing 25 changed files with 663 additions and 524 deletions.
23 changes: 0 additions & 23 deletions src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs

This file was deleted.

137 changes: 137 additions & 0 deletions src/Flurl.Http/Configuration/FlurlClientBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;

namespace Flurl.Http.Configuration
{
/// <summary>
/// A builder for configuring IFlurlClient instances.
/// </summary>
public interface IFlurlClientBuilder
{
/// <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.
/// </summary>
#if NETCOREAPP2_1_OR_GREATER
IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configure);
#else
IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configure);
#endif

/// <summary>
/// Add a provided DelegatingHandler to the IFlurlClient.
/// </summary>
IFlurlClientBuilder AddMiddleware(Func<DelegatingHandler> create);

/// <summary>
/// Builds an instance of IFlurlClient based on configurations specified.
/// </summary>
IFlurlClient Build();
}

/// <summary>
/// Default implementation of IFlurlClientBuilder.
/// </summary>
public class FlurlClientBuilder : IFlurlClientBuilder
{
private readonly IFlurlClientFactory _factory;
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

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

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

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

/// <inheritdoc />
public IFlurlClientBuilder AddMiddleware(Func<DelegatingHandler> create) {
_addMiddleware.Add(create);
return this;
}

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

/// <inheritdoc />
#if NETCOREAPP2_1_OR_GREATER
public IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configure) {
#else
public IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configure) {
#endif
_handlerBuilder.Configs.Add(configure);
return this;
}

/// <inheritdoc />
public IFlurlClient Build() {
var outerHandler = _handlerBuilder.Build(_factory);

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)
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;
}
}
}
}
94 changes: 94 additions & 0 deletions src/Flurl.Http/Configuration/FlurlClientCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Collections.Concurrent;

namespace Flurl.Http.Configuration
{
/// <summary>
/// Interface for a cache of IFlurlClient instances.
/// </summary>
public interface IFlurlClientCache
{
/// <summary>
/// Adds and returns a new IFlurlClient. 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>
IFlurlClientBuilder Add(string name, string baseUrl = null);

/// <summary>
/// Gets a named IFlurlClient, creating one if it doesn't exist or has been disposed.
/// </summary>
/// <param name="name">The client name.</param>
/// <returns></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.
/// </summary>
IFlurlClientCache ConfigureAll(Action<IFlurlClientBuilder> configure);

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

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

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

/// <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.");

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

/// <inheritdoc />
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;
}

private Lazy<IFlurlClient> CreateLazyInstance(FlurlClientBuilder builder) {
_configureAll?.Invoke(builder);
return new Lazy<IFlurlClient>(builder.Build);
}

/// <inheritdoc />
public IFlurlClientCache ConfigureAll(Action<IFlurlClientBuilder> configure) {
_configureAll = configure;
return this;
}

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

/// <inheritdoc />
public void 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);
}
}
}
77 changes: 77 additions & 0 deletions src/Flurl.Http/Configuration/FlurlClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Net;
using System.Net.Http;

namespace Flurl.Http.Configuration
{
/// <summary>
/// Interface for helper methods used to construct IFlurlClient instances.
/// </summary>
public interface IFlurlClientFactory
{
/// <summary>
/// Creates and configures a new HttpClient as needed when a new IFlurlClient instance is created.
/// Implementors should NOT attempt to cache or reuse HttpClient instances here - their lifetime is
/// bound one-to-one with an IFlurlClient, whose caching and reuse is managed by IFlurlClientCache.
/// </summary>
/// <param name="handler">The HttpMessageHandler passed to the constructor of the HttpClient.</param>
HttpClient CreateHttpClient(HttpMessageHandler handler);

/// <summary>
/// Creates and configures a new HttpMessageHandler as needed when a new IFlurlClient instance is created.
/// The default implementation creates an instance of SocketsHttpHandler for platforms that support it,
/// otherwise HttpClientHandler.
/// </summary>
HttpMessageHandler CreateInnerHandler();
}

/// <summary>
/// Extension methods on IFlurlClientFactory
/// </summary>
public static class FlurlClientFactoryExtensions
{
/// <summary>
/// Creates an HttpClient with the HttpMessageHandler returned from this factory's CreateInnerHandler method.
/// </summary>
public static HttpClient CreateHttpClient(this IFlurlClientFactory fac) => fac.CreateHttpClient(fac.CreateInnerHandler());
}

/// <summary>
/// Default implementation of IFlurlClientFactory, used to build and cache IFlurlClient instances.
/// </summary>
public class DefaultFlurlClientFactory : IFlurlClientFactory
{
/// <inheritdoc />
public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) {
return new HttpClient(handler);
}

/// <summary>
/// Creates and configures a new HttpMessageHandler as needed when a new IFlurlClient instance is created.
/// </summary>
public virtual HttpMessageHandler CreateInnerHandler() {
// Flurl has its own mechanisms for managing cookies and redirects, so we need to disable them in the inner handler.
#if NETCOREAPP2_1_OR_GREATER
var handler = new SocketsHttpHandler {
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
#else
var handler = new HttpClientHandler();

if (handler.SupportsRedirectConfiguration)
handler.AllowAutoRedirect = false;

// #266
// deflate not working? see #474
if (handler.SupportsAutomaticDecompression)
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;

try { handler.UseCookies = false; }
catch (PlatformNotSupportedException) { } // look out for WASM platforms (#543)
#endif
return handler;
}
}
}
Loading

0 comments on commit b03f98e

Please sign in to comment.