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

Flurl.Http 4.0-pre5 #771

Merged
merged 9 commits into from
Oct 22, 2023
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
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