diff --git a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs
deleted file mode 100644
index f7899157..00000000
--- a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Flurl.Http.Configuration
-{
- ///
- /// An IFlurlClientFactory implementation that caches and reuses the same one instance of
- /// FlurlClient per combination of scheme, host, and port. This is the default
- /// implementation used when calls are made fluently off Urls/strings.
- ///
- public class DefaultFlurlClientFactory : FlurlClientFactoryBase
- {
- ///
- /// Returns a unique cache key based on scheme, host, and port of the given URL.
- ///
- /// The URL.
- /// The cache key
- protected override string GetCacheKey(Url url) => $"{url.Scheme}|{url.Host}|{url.Port}";
- }
-}
\ No newline at end of file
diff --git a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs
new file mode 100644
index 00000000..1459f633
--- /dev/null
+++ b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+
+namespace Flurl.Http.Configuration
+{
+ ///
+ /// A builder for configuring IFlurlClient instances.
+ ///
+ public interface IFlurlClientBuilder
+ {
+ ///
+ /// Configure the IFlurlClient's Settings.
+ ///
+ IFlurlClientBuilder WithSettings(Action configure);
+
+ ///
+ /// Configure the HttpClient wrapped by this IFlurlClient.
+ ///
+ IFlurlClientBuilder ConfigureHttpClient(Action configure);
+
+ ///
+ /// Configure the inner-most HttpMessageHandler associated with this IFlurlClient.
+ ///
+#if NETCOREAPP2_1_OR_GREATER
+ IFlurlClientBuilder ConfigureInnerHandler(Action configure);
+#else
+ IFlurlClientBuilder ConfigureInnerHandler(Action configure);
+#endif
+
+ ///
+ /// Add a provided DelegatingHandler to the IFlurlClient.
+ ///
+ IFlurlClientBuilder AddMiddleware(Func create);
+
+ ///
+ /// Builds an instance of IFlurlClient based on configurations specified.
+ ///
+ IFlurlClient Build();
+ }
+
+ ///
+ /// Default implementation of IFlurlClientBuilder.
+ ///
+ public class FlurlClientBuilder : IFlurlClientBuilder
+ {
+ private readonly IFlurlClientFactory _factory;
+ private readonly string _baseUrl;
+ private readonly List> _addMiddleware = new();
+ private readonly List> _configSettings = new();
+ private readonly List> _configClient = new();
+#if NETCOREAPP2_1_OR_GREATER
+ private readonly HandlerBuilder _handlerBuilder = new();
+#else
+ private readonly HandlerBuilder _handlerBuilder = new();
+#endif
+
+ ///
+ /// Creates a new FlurlClientBuilder.
+ ///
+ public FlurlClientBuilder(string baseUrl = null) : this(new DefaultFlurlClientFactory(), baseUrl) { }
+
+ ///
+ /// Creates a new FlurlClientBuilder.
+ ///
+ internal FlurlClientBuilder(IFlurlClientFactory factory, string baseUrl) {
+ _factory = factory;
+ _baseUrl = baseUrl;
+ }
+
+ ///
+ public IFlurlClientBuilder WithSettings(Action configure) {
+ _configSettings.Add(configure);
+ return this;
+ }
+
+ ///
+ public IFlurlClientBuilder AddMiddleware(Func create) {
+ _addMiddleware.Add(create);
+ return this;
+ }
+
+ ///
+ public IFlurlClientBuilder ConfigureHttpClient(Action configure) {
+ _configClient.Add(configure);
+ return this;
+ }
+
+ ///
+#if NETCOREAPP2_1_OR_GREATER
+ public IFlurlClientBuilder ConfigureInnerHandler(Action configure) {
+#else
+ public IFlurlClientBuilder ConfigureInnerHandler(Action configure) {
+#endif
+ _handlerBuilder.Configs.Add(configure);
+ return this;
+ }
+
+ ///
+ 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 where T : HttpMessageHandler
+ {
+ public List> 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;
+ }
+ }
+ }
+}
diff --git a/src/Flurl.Http/Configuration/FlurlClientCache.cs b/src/Flurl.Http/Configuration/FlurlClientCache.cs
new file mode 100644
index 00000000..5da8da86
--- /dev/null
+++ b/src/Flurl.Http/Configuration/FlurlClientCache.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Concurrent;
+
+namespace Flurl.Http.Configuration
+{
+ ///
+ /// Interface for a cache of IFlurlClient instances.
+ ///
+ public interface IFlurlClientCache
+ {
+ ///
+ /// Adds and returns a new IFlurlClient. Call once per client at startup to register and configure a named client.
+ ///
+ /// Name of the IFlurlClient. Serves as a cache key. Subsequent calls to Get will return this client.
+ /// Optional. The base URL associated with the new client.
+ ///
+ IFlurlClientBuilder Add(string name, string baseUrl = null);
+
+ ///
+ /// Gets a named IFlurlClient, creating one if it doesn't exist or has been disposed.
+ ///
+ /// The client name.
+ ///
+ IFlurlClient Get(string name);
+
+ ///
+ /// 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.
+ ///
+ IFlurlClientCache ConfigureAll(Action configure);
+
+ ///
+ /// Removes a named client from this cache.
+ ///
+ void Remove(string name);
+
+ ///
+ /// Disposes and removes all cached IFlurlClient instances.
+ ///
+ void Clear();
+ }
+
+ ///
+ /// Default implementation of IFlurlClientCache.
+ ///
+ public class FlurlClientCache : IFlurlClientCache {
+ private readonly ConcurrentDictionary> _clients = new();
+ private readonly IFlurlClientFactory _factory = new DefaultFlurlClientFactory();
+ private Action _configureAll;
+
+ ///
+ 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;
+ }
+
+ ///
+ public virtual IFlurlClient Get(string name) {
+ if (name == null)
+ throw new ArgumentNullException(nameof(name));
+
+ Lazy Create() => CreateLazyInstance(new FlurlClientBuilder(_factory, null));
+ return _clients.AddOrUpdate(name, _ => Create(), (_, existing) => existing.Value.IsDisposed ? Create() : existing).Value;
+ }
+
+ private Lazy CreateLazyInstance(FlurlClientBuilder builder) {
+ _configureAll?.Invoke(builder);
+ return new Lazy(builder.Build);
+ }
+
+ ///
+ public IFlurlClientCache ConfigureAll(Action configure) {
+ _configureAll = configure;
+ return this;
+ }
+
+ ///
+ public void Remove(string name) {
+ if (_clients.TryRemove(name, out var cli) && cli.IsValueCreated && !cli.Value.IsDisposed)
+ cli.Value.Dispose();
+ }
+
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Flurl.Http/Configuration/FlurlClientFactory.cs b/src/Flurl.Http/Configuration/FlurlClientFactory.cs
new file mode 100644
index 00000000..2c27bac5
--- /dev/null
+++ b/src/Flurl.Http/Configuration/FlurlClientFactory.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Net;
+using System.Net.Http;
+
+namespace Flurl.Http.Configuration
+{
+ ///
+ /// Interface for helper methods used to construct IFlurlClient instances.
+ ///
+ public interface IFlurlClientFactory
+ {
+ ///
+ /// 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.
+ ///
+ /// The HttpMessageHandler passed to the constructor of the HttpClient.
+ HttpClient CreateHttpClient(HttpMessageHandler handler);
+
+ ///
+ /// 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.
+ ///
+ HttpMessageHandler CreateInnerHandler();
+ }
+
+ ///
+ /// Extension methods on IFlurlClientFactory
+ ///
+ public static class FlurlClientFactoryExtensions
+ {
+ ///
+ /// Creates an HttpClient with the HttpMessageHandler returned from this factory's CreateInnerHandler method.
+ ///
+ public static HttpClient CreateHttpClient(this IFlurlClientFactory fac) => fac.CreateHttpClient(fac.CreateInnerHandler());
+ }
+
+ ///
+ /// Default implementation of IFlurlClientFactory, used to build and cache IFlurlClient instances.
+ ///
+ public class DefaultFlurlClientFactory : IFlurlClientFactory
+ {
+ ///
+ public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) {
+ return new HttpClient(handler);
+ }
+
+ ///
+ /// Creates and configures a new HttpMessageHandler as needed when a new IFlurlClient instance is created.
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs
deleted file mode 100644
index ab158d0d..00000000
--- a/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Net.Http;
-using System.Net;
-
-namespace Flurl.Http.Configuration
-{
- ///
- /// Encapsulates a creation/caching strategy for IFlurlClient instances. Custom factories looking to extend
- /// Flurl's behavior should inherit from this class, rather than implementing IFlurlClientFactory directly.
- ///
- public abstract class FlurlClientFactoryBase : IFlurlClientFactory
- {
- private readonly ConcurrentDictionary _clients = new ConcurrentDictionary();
-
- ///
- /// By default, uses a caching strategy of one FlurlClient per host. This maximizes reuse of
- /// underlying HttpClient/Handler while allowing things like cookies to be host-specific.
- ///
- /// The URL.
- /// The FlurlClient instance.
- public virtual IFlurlClient Get(Url url) {
- if (url == null)
- throw new ArgumentNullException(nameof(url));
-
- return _clients.AddOrUpdate(
- GetCacheKey(url),
- u => Create(u),
- (u, client) => client.IsDisposed ? Create(u) : client);
- }
-
- ///
- /// Defines a strategy for getting a cache key based on a Url. Default implementation
- /// returns the host part (i.e www.api.com) so that all calls to the same host use the
- /// same FlurlClient (and HttpClient/HttpMessageHandler) instance.
- ///
- /// The URL.
- /// The cache key
- protected abstract string GetCacheKey(Url url);
-
- ///
- /// Creates a new FlurlClient
- ///
- /// The URL (not used)
- ///
- protected virtual IFlurlClient Create(Url url) => new FlurlClient();
-
- ///
- /// Disposes all cached IFlurlClient instances and clears the cache.
- ///
- public void Dispose() {
- foreach (var kv in _clients) {
- if (!kv.Value.IsDisposed)
- kv.Value.Dispose();
- }
- _clients.Clear();
- }
-
- ///
- /// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls
- /// (except when one is passed explicitly to the FlurlClient constructor). In order not to lose
- /// Flurl.Http functionality, it is recommended to call base.CreateClient and customize the result.
- ///
- public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) {
- return new HttpClient(handler) {
- // Timeouts handled per request via FlurlHttpSettings.Timeout
- Timeout = System.Threading.Timeout.InfiniteTimeSpan
- };
- }
-
- ///
- /// Override in custom factory to customize the creation of the top-level HttpMessageHandler used in all
- /// Flurl HTTP calls (except when an HttpClient is passed explicitly to the FlurlClient constructor).
- /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler, and
- /// either customize the returned HttpClientHandler, or set it as the InnerHandler of a DelegatingHandler.
- ///
- public virtual HttpMessageHandler CreateMessageHandler() {
- var httpClientHandler = new HttpClientHandler();
-
- // flurl has its own mechanisms for managing cookies and redirects
-
- try { httpClientHandler.UseCookies = false; }
- catch (PlatformNotSupportedException) { } // look out for WASM platforms (#543)
-
- if (httpClientHandler.SupportsRedirectConfiguration)
- httpClientHandler.AllowAutoRedirect = false;
-
- if (httpClientHandler.SupportsAutomaticDecompression) {
- // #266
- // deflate not working? see #474
- httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
- }
- return httpClientHandler;
- }
- }
-}
diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs
index f763284f..760bbc4b 100644
--- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs
+++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
@@ -11,11 +11,23 @@ namespace Flurl.Http.Configuration
///
public class FlurlHttpSettings
{
+ private static readonly FlurlHttpSettings Defaults = new() {
+ Timeout = TimeSpan.FromSeconds(100), // same as HttpClient
+ HttpVersion = "1.1",
+ JsonSerializer = new DefaultJsonSerializer(),
+ UrlEncodedSerializer = new DefaultUrlEncodedSerializer(),
+ Redirects = {
+ Enabled = true,
+ AllowSecureToInsecure = false,
+ ForwardHeaders = false,
+ ForwardAuthorizationHeader = false,
+ MaxAutoRedirects = 10
+ }
+ };
+
// Values are dictionary-backed so we can check for key existence. Can't do null-coalescing
// because if a setting is set to null at the request level, that should stick.
- private readonly IDictionary _vals = new Dictionary();
-
- private FlurlHttpSettings _defaults;
+ private IDictionary _vals = new Dictionary();
///
/// Creates a new FlurlHttpSettings object.
@@ -28,10 +40,7 @@ public FlurlHttpSettings() {
///
/// Gets or sets the default values to fall back on when values are not explicitly set on this instance.
///
- public virtual FlurlHttpSettings Defaults {
- get => _defaults ?? FlurlHttp.GlobalSettings;
- set => _defaults = value;
- }
+ internal FlurlHttpSettings Parent { get; set; }
///
/// Gets or sets the HTTP request timeout.
@@ -154,7 +163,7 @@ public Func OnRedirectAsync {
/// Resets all overridden settings to their default values. For example, on a FlurlRequest,
/// all settings are reset to FlurlClient-level settings.
///
- public virtual void ResetDefaults() {
+ public void ResetDefaults() {
_vals.Clear();
}
@@ -162,12 +171,18 @@ public virtual void ResetDefaults() {
/// Gets a settings value from this instance if explicitly set, otherwise from the default settings that back this instance.
///
internal T Get([CallerMemberName]string propName = null) {
- var testVals = HttpTest.Current?.Settings._vals;
- return
- testVals?.ContainsKey(propName) == true ? (T)testVals[propName] :
- _vals.ContainsKey(propName) ? (T)_vals[propName] :
- Defaults != null ? (T)Defaults.Get(propName) :
- default;
+ IEnumerable prioritize() {
+ yield return HttpTest.Current?.Settings;
+ for (var settings = this; settings != null; settings = settings.Parent)
+ yield return settings;
+ yield return Defaults;
+ }
+
+ foreach (var settings in prioritize())
+ if (settings?._vals?.TryGetValue(propName, out var val) == true)
+ return (T)val;
+
+ return default; // should never get this far assuming Defaults is fully populated
}
///
@@ -177,48 +192,4 @@ internal void Set(T value, [CallerMemberName]string propName = null) {
_vals[propName] = value;
}
}
-
- ///
- /// Global default settings for Flurl.Http
- ///
- public class GlobalFlurlHttpSettings : FlurlHttpSettings
- {
- internal GlobalFlurlHttpSettings() {
- ResetDefaults();
- }
-
- ///
- /// Defaults at the global level do not make sense and will always be null.
- ///
- public override FlurlHttpSettings Defaults {
- get => null;
- set => throw new Exception("Global settings cannot be backed by any higher-level defaults.");
- }
-
- ///
- /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances and,
- /// by proxy, HttpClient instances.
- ///
- public IFlurlClientFactory FlurlClientFactory {
- get => Get();
- set => Set(value);
- }
-
- ///
- /// Resets all global settings to their default values.
- ///
- public override void ResetDefaults() {
- base.ResetDefaults();
- Timeout = TimeSpan.FromSeconds(100); // same as HttpClient
- HttpVersion = "1.1";
- JsonSerializer = new DefaultJsonSerializer();
- UrlEncodedSerializer = new DefaultUrlEncodedSerializer();
- FlurlClientFactory = new DefaultFlurlClientFactory();
- Redirects.Enabled = true;
- Redirects.AllowSecureToInsecure = false;
- Redirects.ForwardHeaders = false;
- Redirects.ForwardAuthorizationHeader = false;
- Redirects.MaxAutoRedirects = 10;
- }
- }
}
diff --git a/src/Flurl.Http/Configuration/IFlurlClientFactory.cs b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs
deleted file mode 100644
index 49805848..00000000
--- a/src/Flurl.Http/Configuration/IFlurlClientFactory.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Runtime.CompilerServices;
-
-namespace Flurl.Http.Configuration
-{
- ///
- /// Interface for defining a strategy for creating, caching, and reusing IFlurlClient instances and
- /// their underlying HttpClient instances. It is generally preferable to derive from FlurlClientFactoryBase
- /// and only override methods as needed, rather than implementing this interface from scratch.
- ///
- public interface IFlurlClientFactory : IDisposable
- {
- ///
- /// Strategy to create a FlurlClient or reuse an existing one, based on the URL being called.
- ///
- /// The URL being called.
- ///
- IFlurlClient Get(Url url);
-
- ///
- /// Defines how HttpClient should be instantiated and configured by default. Do NOT attempt
- /// to cache/reuse HttpClient instances here - that should be done at the FlurlClient level
- /// via a custom FlurlClientFactory that gets registered globally.
- ///
- /// The HttpMessageHandler used to construct the HttpClient.
- ///
- HttpClient CreateHttpClient(HttpMessageHandler handler);
-
- ///
- /// Defines how the HttpMessageHandler used by HttpClients that are created by
- /// this factory should be instantiated and configured.
- ///
- ///
- HttpMessageHandler CreateMessageHandler();
- }
-
- ///
- /// Extension methods on IFlurlClientFactory
- ///
- public static class FlurlClientFactoryExtensions
- {
- // https://stackoverflow.com/questions/51563732/how-do-i-lock-when-the-ideal-scope-of-the-lock-object-is-known-only-at-runtime
- private static readonly ConditionalWeakTable _clientLocks = new ConditionalWeakTable();
-
- ///
- /// Provides thread-safe access to a specific IFlurlClient, typically to configure settings and default headers.
- /// The URL is used to find the client, but keep in mind that the same client will be used in all calls to the same host by default.
- ///
- /// This IFlurlClientFactory.
- /// the URL used to find the IFlurlClient.
- /// the action to perform against the IFlurlClient.
- public static IFlurlClientFactory ConfigureClient(this IFlurlClientFactory factory, string url, Action configAction) {
- var client = factory.Get(url);
- lock (_clientLocks.GetOrCreateValue(client)) {
- configAction(client);
- }
- return factory;
- }
-
- ///
- /// Creates an HttpClient with the HttpMessageHandler returned from this factory's CreateMessageHandler method.
- ///
- public static HttpClient CreateHttpClient(this IFlurlClientFactory fac) => fac.CreateHttpClient(fac.CreateMessageHandler());
- }
-}
\ No newline at end of file
diff --git a/src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs b/src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs
deleted file mode 100644
index b599b940..00000000
--- a/src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Flurl.Http.Configuration
-{
- ///
- /// An IFlurlClientFactory implementation that caches and reuses the same IFlurlClient instance
- /// per URL requested, which it assumes is a "base" URL, and sets the IFlurlClient.BaseUrl property
- /// to that value. Ideal for use with IoC containers - register as a singleton, inject into a service
- /// that wraps some web service, and use to set a private IFlurlClient field in the constructor.
- ///
- public class PerBaseUrlFlurlClientFactory : FlurlClientFactoryBase
- {
- ///
- /// Returns the entire URL, which is assumed to be some "base" URL for a service.
- ///
- /// The URL.
- /// The cache key
- protected override string GetCacheKey(Url url) => url.ToString();
-
- ///
- /// Returns a new new FlurlClient with BaseUrl set to the URL passed.
- ///
- /// The URL
- ///
- protected override IFlurlClient Create(Url url) => new FlurlClient(url);
- }
-}
diff --git a/src/Flurl.Http/Content/CapturedMultipartContent.cs b/src/Flurl.Http/Content/CapturedMultipartContent.cs
index 827308ba..44b76827 100644
--- a/src/Flurl.Http/Content/CapturedMultipartContent.cs
+++ b/src/Flurl.Http/Content/CapturedMultipartContent.cs
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
-using System.Text;
using Flurl.Http.Configuration;
using Flurl.Util;
@@ -16,7 +14,7 @@ namespace Flurl.Http.Content
public class CapturedMultipartContent : MultipartContent
{
private readonly FlurlHttpSettings _settings;
- private readonly List _capturedParts = new List();
+ private readonly List _capturedParts = new();
///
/// Gets an array of HttpContent objects that make up the parts of the multipart request.
@@ -26,18 +24,18 @@ public class CapturedMultipartContent : MultipartContent
///
/// Initializes a new instance of the class.
///
- /// The FlurlHttpSettings used to serialize each content part. (Defaults to FlurlHttp.GlobalSettings.)
- public CapturedMultipartContent(FlurlHttpSettings settings = null) : base("form-data") {
- _settings = settings ?? FlurlHttp.GlobalSettings;
+ /// The FlurlHttpSettings used to serialize each content part.
+ public CapturedMultipartContent(FlurlHttpSettings settings) : base("form-data") {
+ _settings = settings;
}
///
/// Initializes a new instance of the class.
///
/// The subtype of the multipart content.
- /// The FlurlHttpSettings used to serialize each content part. (Defaults to FlurlHttp.GlobalSettings.)
- public CapturedMultipartContent(string subtype, FlurlHttpSettings settings = null) : base(subtype) {
- _settings = settings ?? FlurlHttp.GlobalSettings;
+ /// The FlurlHttpSettings used to serialize each content part.
+ public CapturedMultipartContent(string subtype, FlurlHttpSettings settings) : base(subtype) {
+ _settings = settings;
}
///
@@ -45,9 +43,9 @@ public CapturedMultipartContent(string subtype, FlurlHttpSettings settings = nul
///
/// The subtype of the multipart content.
/// The boundary string for the multipart content.
- /// The FlurlHttpSettings used to serialize each content part. (Defaults to FlurlHttp.GlobalSettings.)
- public CapturedMultipartContent(string subtype, string boundary, FlurlHttpSettings settings = null) : base(subtype, boundary) {
- _settings = settings ?? FlurlHttp.GlobalSettings;
+ /// The FlurlHttpSettings used to serialize each content part.
+ public CapturedMultipartContent(string subtype, string boundary, FlurlHttpSettings settings) : base(subtype, boundary) {
+ _settings = settings;
}
///
diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj
index 906bd5fe..607cdcfe 100644
--- a/src/Flurl.Http/Flurl.Http.csproj
+++ b/src/Flurl.Http/Flurl.Http.csproj
@@ -4,7 +4,7 @@
9.0
True
Flurl.Http
- 4.0.0-pre4
+ 4.0.0-pre5
Todd Menier
A fluent, portable, testable HTTP client library.
https://flurl.dev
diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs
index 7cd57abd..d2b2843f 100644
--- a/src/Flurl.Http/FlurlClient.cs
+++ b/src/Flurl.Http/FlurlClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@@ -14,8 +14,7 @@ namespace Flurl.Http
///
public interface IFlurlClient : IHttpSettingsContainer, IDisposable {
///
- /// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated
- /// to FlurlHttp.FlurlClientFactory. Reused for the life of the FlurlClient.
+ /// Gets the HttpClient that this IFlurlClient wraps.
///
HttpClient HttpClient { get; }
@@ -51,15 +50,16 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable {
///
public class FlurlClient : IFlurlClient
{
+ private static readonly Lazy _defaultFactory = new(() => new DefaultFlurlClientFactory());
+
///
- /// Initializes a new instance of .
+ /// Creates a new instance of .
///
/// The base URL associated with this client.
- public FlurlClient(string baseUrl = null) :
- this(FlurlHttp.GlobalSettings.FlurlClientFactory.CreateHttpClient(), baseUrl) { }
+ public FlurlClient(string baseUrl = null) : this(_defaultFactory.Value.CreateHttpClient(), baseUrl) { }
///
- /// Initializes a new instance of , wrapping an existing HttpClient.
+ /// Creates a new instance of , wrapping an existing HttpClient.
/// Generally, you should let Flurl create and manage HttpClient instances for you, but you might, for
/// example, have an HttpClient instance that was created by a 3rd-party library and you want to use
/// Flurl to build and send calls with it. Be aware that if the HttpClient has an underlying
@@ -83,7 +83,7 @@ public FlurlClient(HttpClient httpClient, string baseUrl = null) {
public string BaseUrl { get; set; }
///
- public FlurlHttpSettings Settings { get; } = new FlurlHttpSettings();
+ public FlurlHttpSettings Settings { get; } = new();
///
public INameValueList Headers { get; } = new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600
@@ -192,7 +192,7 @@ private async Task ProcessRedirectAsync(FlurlCall call, HttpComp
Verb = changeToGet ? HttpMethod.Get : call.HttpRequestMessage.Method,
Content = changeToGet ? null : call.Request.Content,
RedirectedFrom = call,
- Settings = { Defaults = settings }
+ Settings = { Parent = settings }
};
if (call.Request.CookieJar != null)
diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs
index bd24346b..937f5dd4 100644
--- a/src/Flurl.Http/FlurlHttp.cs
+++ b/src/Flurl.Http/FlurlHttp.cs
@@ -4,37 +4,46 @@
namespace Flurl.Http
{
///
- /// A static container for global configuration settings affecting Flurl.Http behavior.
+ /// A static object for configuring Flurl for "clientless" usage. Provides a default IFlurlClientCache instance primarily
+ /// for clientless support, but can be used directly, as an alternative to a DI-managed singleton cache.
///
public static class FlurlHttp
{
- private static readonly object _configLock = new object();
+ private static Func _cachingStrategy = BuildClientNameByHost;
- private static Lazy _settings =
- new Lazy(() => new GlobalFlurlHttpSettings());
+ ///
+ /// A global collection of cached IFlurlClient instances.
+ ///
+ public static IFlurlClientCache Clients { get; } = new FlurlClientCache();
+
+ ///
+ /// Gets a builder for configuring the IFlurlClient that would be selected for calling the given URL when the clientless pattern is used.
+ /// Note that if you've overridden the caching strategy to vary clients by request properties other than Url, you should instead use
+ /// FlurlHttp.Clients.Add(name) to ensure you are configuring the correct client.
+ ///
+ public static IFlurlClientBuilder ConfigureClientForUrl(string url) => Clients.Add(_cachingStrategy(new FlurlRequest(url)));
+
+ ///
+ /// Gets or creates the IFlurlClient that would be selected for sending the given IFlurlRequest when the clientless pattern is used.
+ ///
+ public static IFlurlClient GetClientForRequest(IFlurlRequest req) => Clients.Get(_cachingStrategy(req));
///
- /// Globally configured Flurl.Http settings. Should normally be written to by calling FlurlHttp.Configure once application at startup.
+ /// Sets a global caching strategy for getting or creating an IFlurlClient instance when the clientless pattern is used, e.g. url.GetAsync.
///
- public static GlobalFlurlHttpSettings GlobalSettings => _settings.Value;
+ /// A delegate that returns a cache key used to store and retrieve a client instance based on properties of the request.
+ public static void UseClientCachingStrategy(Func buildClientName) => _cachingStrategy = buildClientName;
///
- /// Provides thread-safe access to Flurl.Http's global configuration settings. Should only be called once at application startup.
+ /// Sets a global caching strategy of one IFlurlClient per scheme/host/port combination when the clientless pattern is used,
+ /// e.g. url.GetAsync. This is the default strategy, so you shouldn't need to call this except to revert a previous call to
+ /// UseClientCachingStrategy, which would be rare.
///
- /// the action to perform against the GlobalSettings.
- public static void Configure(Action configAction) {
- lock (_configLock) {
- configAction(GlobalSettings);
- }
- }
+ public static void UseClientPerHostStrategy() => _cachingStrategy = BuildClientNameByHost;
///
- /// Provides thread-safe access to a specific IFlurlClient, typically to configure settings and default headers.
- /// The URL is used to find the client, but keep in mind that the same client will be used in all calls to the same host by default.
+ /// Builds a cache key consisting of URL scheme, host, and port. This is the default client caching strategy.
///
- /// the URL used to find the IFlurlClient.
- /// the action to perform against the IFlurlClient.
- public static void ConfigureClient(string url, Action configAction) =>
- GlobalSettings.FlurlClientFactory.ConfigureClient(url, configAction);
+ public static string BuildClientNameByHost(IFlurlRequest req) => $"{req.Url.Scheme}|{req.Url.Host}|{req.Url.Port}";
}
}
\ No newline at end of file
diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs
index 558e0908..6d52f2e1 100644
--- a/src/Flurl.Http/FlurlRequest.cs
+++ b/src/Flurl.Http/FlurlRequest.cs
@@ -98,14 +98,14 @@ internal FlurlRequest(string baseUrl, params object[] urlSegments) {
}
///
- public FlurlHttpSettings Settings { get; } = new FlurlHttpSettings();
+ public FlurlHttpSettings Settings { get; } = new();
///
public IFlurlClient Client {
get => _client;
set {
_client = value;
- Settings.Defaults = _client?.Settings;
+ Settings.Parent = _client?.Settings;
}
}
@@ -138,7 +138,7 @@ public CookieJar CookieJar {
public Task SendAsync(HttpMethod verb, HttpContent content = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
Verb = verb;
Content = content;
- Client ??= FlurlHttp.GlobalSettings.FlurlClientFactory.Get(Url);
+ Client ??= FlurlHttp.GetClientForRequest(this);
return Client.SendAsync(this, completionOption, cancellationToken);
}
diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/SettingsExtensions.cs
index 24489200..b5c3a06f 100644
--- a/src/Flurl.Http/SettingsExtensions.cs
+++ b/src/Flurl.Http/SettingsExtensions.cs
@@ -17,7 +17,7 @@ public static class SettingsExtensions
/// The IFlurlClient.
/// Action defining the settings changes.
/// The IFlurlClient with the modified Settings
- public static IFlurlClient Configure(this IFlurlClient client, Action action) {
+ public static IFlurlClient WithSettings(this IFlurlClient client, Action action) {
action(client.Settings);
return client;
}
diff --git a/src/Flurl.Http/Testing/HttpCallAssertion.cs b/src/Flurl.Http/Testing/HttpCallAssertion.cs
index e96b0f6a..150f8bb9 100644
--- a/src/Flurl.Http/Testing/HttpCallAssertion.cs
+++ b/src/Flurl.Http/Testing/HttpCallAssertion.cs
@@ -13,6 +13,7 @@ namespace Flurl.Http.Testing
///
public class HttpCallAssertion
{
+ private readonly HttpTest _test;
private readonly bool _negate;
private readonly IList _expectedConditions = new List();
@@ -21,10 +22,11 @@ public class HttpCallAssertion
///
/// Constructs a new instance of HttpCallAssertion.
///
- /// Set of calls (usually from HttpTest.CallLog) to assert against.
+ /// The HttpTest containing calls being asserted.
/// If true, assertions pass when calls matching criteria were NOT made.
- public HttpCallAssertion(IEnumerable loggedCalls, bool negate = false) {
- _calls = loggedCalls.ToList();
+ public HttpCallAssertion(HttpTest test, bool negate = false) {
+ _test = test;
+ _calls = test.CallLog.ToList();
_negate = negate;
}
@@ -99,7 +101,7 @@ public HttpCallAssertion WithRequestBody(string bodyPattern) {
/// Asserts whether calls were made containing given JSON-encoded request body. body may contain * wildcard.
///
public HttpCallAssertion WithRequestJson(object body) {
- var serializedBody = FlurlHttp.GlobalSettings.JsonSerializer.Serialize(body);
+ var serializedBody = _test.Settings.JsonSerializer.Serialize(body);
return WithRequestBody(serializedBody);
}
@@ -107,7 +109,7 @@ public HttpCallAssertion WithRequestJson(object body) {
/// Asserts whether calls were made containing given URL-encoded request body. body may contain * wildcard.
///
public HttpCallAssertion WithRequestUrlEncoded(object body) {
- var serializedBody = FlurlHttp.GlobalSettings.UrlEncodedSerializer.Serialize(body);
+ var serializedBody = _test.Settings.UrlEncodedSerializer.Serialize(body);
return WithRequestBody(serializedBody);
}
#endregion
diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs
index c241826d..43a0c0a6 100644
--- a/src/Flurl.Http/Testing/HttpTest.cs
+++ b/src/Flurl.Http/Testing/HttpTest.cs
@@ -15,8 +15,8 @@ namespace Flurl.Http.Testing
[Serializable]
public class HttpTest : HttpTestSetup, IDisposable
{
- private readonly ConcurrentQueue _calls = new ConcurrentQueue();
- private readonly List _filteredSetups = new List();
+ private readonly ConcurrentQueue _calls = new();
+ private readonly List _filteredSetups = new();
///
/// Initializes a new instance of the class.
@@ -43,7 +43,7 @@ public HttpTest() : base(new FlurlHttpSettings()) {
///
/// Action defining the settings changes.
/// This HttpTest
- public HttpTest Configure(Action action) {
+ public HttpTest WithSettings(Action action) {
action(Settings);
return this;
}
@@ -66,7 +66,7 @@ internal HttpTestSetup FindSetup(FlurlCall call) {
///
/// URL that should have been called. Can include * wildcard character.
public HttpCallAssertion ShouldHaveCalled(string urlPattern) {
- return new HttpCallAssertion(CallLog).WithUrlPattern(urlPattern);
+ return new HttpCallAssertion(this).WithUrlPattern(urlPattern);
}
///
@@ -74,21 +74,21 @@ public HttpCallAssertion ShouldHaveCalled(string urlPattern) {
///
/// URL that should not have been called. Can include * wildcard character.
public void ShouldNotHaveCalled(string urlPattern) {
- new HttpCallAssertion(CallLog, true).WithUrlPattern(urlPattern);
+ new HttpCallAssertion(this, true).WithUrlPattern(urlPattern);
}
///
/// Asserts whether any HTTP call was made, throwing HttpCallAssertException if none were.
///
public HttpCallAssertion ShouldHaveMadeACall() {
- return new HttpCallAssertion(CallLog).WithUrlPattern("*");
+ return new HttpCallAssertion(this).WithUrlPattern("*");
}
///
/// Asserts whether no HTTP calls were made, throwing HttpCallAssertException if any were.
///
public void ShouldNotHaveMadeACall() {
- new HttpCallAssertion(CallLog, true).WithUrlPattern("*");
+ new HttpCallAssertion(this, true).WithUrlPattern("*");
}
///
diff --git a/test/Flurl.Test/Http/CookieTests.cs b/test/Flurl.Test/Http/CookieTests.cs
index 049e7f3e..6f363241 100644
--- a/test/Flurl.Test/Http/CookieTests.cs
+++ b/test/Flurl.Test/Http/CookieTests.cs
@@ -107,7 +107,7 @@ public void can_build_response_header() {
}
[Test]
- public void can_perist_and_load_jar() {
+ public void can_persist_and_load_jar() {
var jar1 = new CookieJar()
.AddOrReplace("x", "foo", "https://site1.com", DateTimeOffset.UtcNow)
.AddOrReplace("y", "bar", "https://site2.com", DateTimeOffset.UtcNow.AddMinutes(-10));
diff --git a/test/Flurl.Test/Http/FlurlClientBuilderTests.cs b/test/Flurl.Test/Http/FlurlClientBuilderTests.cs
new file mode 100644
index 00000000..1fe97be5
--- /dev/null
+++ b/test/Flurl.Test/Http/FlurlClientBuilderTests.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Flurl.Http;
+using Flurl.Http.Configuration;
+using NUnit.Framework;
+
+namespace Flurl.Test.Http
+{
+ [TestFixture]
+ public class FlurlClientBuilderTests
+ {
+ [Test]
+ public void can_configure_settings() {
+ var builder = new FlurlClientBuilder();
+ var cli = builder.WithSettings(s => s.HttpVersion = "3.0").Build();
+ Assert.AreEqual("3.0", cli.Settings.HttpVersion);
+ }
+
+ [Test]
+ public void can_configure_HttpClient() {
+ var builder = new FlurlClientBuilder();
+ var cli = builder.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://flurl.dev/docs/fluent-http")).Build();
+ Assert.AreEqual("https://flurl.dev/docs/fluent-http", cli.HttpClient.BaseAddress.ToString());
+ Assert.AreEqual("https://flurl.dev/docs/fluent-http", cli.BaseUrl);
+ }
+
+ [Test]
+ public async Task can_configure_inner_handler() {
+ var builder = new FlurlClientBuilder();
+ // Handlers are not accessible beyond HttpClient constructor, making them hard to assert against!
+ var cli = builder.ConfigureInnerHandler(h => h.Dispose()).Build();
+ try {
+ await cli.Request("https://www.google.com").GetAsync();
+ Assert.Fail("Should have failed since the inner handler was disposed.");
+ }
+ catch (FlurlHttpException ex) {
+ Assert.IsInstanceOf(ex.InnerException);
+ StringAssert.EndsWith("Handler", (ex.InnerException as ObjectDisposedException).ObjectName);
+ }
+ }
+
+ [Test]
+ public async Task can_add_middleware() {
+ var builder = new FlurlClientBuilder();
+ var cli = builder.AddMiddleware(() => new BlockingHandler("blocked by flurl!")).Build();
+ var resp = await cli.Request("https://www.google.com").GetStringAsync();
+ Assert.AreEqual("blocked by flurl!", resp);
+ }
+
+ [Test]
+ public void inner_hanlder_is_SocketsHttpHandler_when_supported() {
+ HttpMessageHandler handler = null;
+ new FlurlClientBuilder()
+ .ConfigureInnerHandler(h => handler = h)
+ .Build();
+#if NET
+ Assert.IsInstanceOf(handler);
+#else
+ Assert.IsInstanceOf(handler);
+#endif
+ }
+
+ class BlockingHandler : DelegatingHandler
+ {
+ private readonly string _msg;
+
+ public BlockingHandler(string msg) {
+ _msg = msg;
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
+ return Task.FromResult(new HttpResponseMessage { Content = new StringContent(_msg) });
+ }
+ }
+ }
+}
diff --git a/test/Flurl.Test/Http/FlurlClientCacheTests.cs b/test/Flurl.Test/Http/FlurlClientCacheTests.cs
new file mode 100644
index 00000000..b44ce4ed
--- /dev/null
+++ b/test/Flurl.Test/Http/FlurlClientCacheTests.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Flurl.Http.Configuration;
+using NUnit.Framework;
+
+namespace Flurl.Test.Http
+{
+ [TestFixture]
+ public class FlurlClientCacheTests
+ {
+ [Test]
+ public void can_add_and_get_client() {
+ var cache = new FlurlClientCache();
+ cache.Add("github", "https://api.github.com").WithSettings(s => s.Timeout = TimeSpan.FromSeconds(123));
+ cache.Add("google", "https://api.google.com").WithSettings(s => s.Timeout = TimeSpan.FromSeconds(234));
+
+ var gh = cache.Get("github");
+ Assert.AreEqual("https://api.github.com", gh.BaseUrl);
+ Assert.AreEqual(123, gh.Settings.Timeout.Value.TotalSeconds);
+ Assert.AreSame(gh, cache.Get("github"), "should reuse same-named clients.");
+
+ var goog = cache.Get("google");
+ Assert.AreEqual("https://api.google.com", goog.BaseUrl);
+ Assert.AreEqual(234, goog.Settings.Timeout.Value.TotalSeconds);
+ Assert.AreSame(goog, cache.Get("google"), "should reuse same-named clients.");
+ }
+
+ [Test]
+ public void can_get_unconfigured_client() {
+ var cache = new FlurlClientCache();
+ var cli = cache.Get("foo");
+ Assert.IsNull(cli.BaseUrl);
+ Assert.AreEqual(100, cli.Settings.Timeout.Value.TotalSeconds);
+
+ Assert.AreSame(cli, cache.Get("foo"), "should reuse same-named clients.");
+ Assert.AreNotSame(cli, cache.Get("bar"), "different-named clients should be different instances.");
+ }
+
+ [Test]
+ public void cannot_add_same_name_twice() {
+ var cache = new FlurlClientCache();
+ cache.Add("foo", "https://api.github.com");
+ Assert.Throws(() => cache.Add("foo", "https://api.google.com"));
+ }
+
+ [Test]
+ public void can_configure_all() {
+ var cache = new FlurlClientCache()
+ .ConfigureAll(b => b.WithSettings(s => s.Timeout = TimeSpan.FromSeconds(123)));
+
+ var cli1 = cache.Get("foo");
+
+ cache.Add("bar").WithSettings(s => {
+ s.Timeout = TimeSpan.FromSeconds(456);
+ });
+ cache.ConfigureAll(b => b.WithSettings(s => {
+ s.Timeout = TimeSpan.FromSeconds(789);
+ }));
+
+ var cli2 = cache.Get("bar");
+ var cli3 = cache.Get("buzz");
+
+ Assert.AreEqual(123, cli1.Settings.Timeout.Value.TotalSeconds, "fetched the client before changing the defaults, so original should stick");
+ Assert.AreEqual(456, cli2.Settings.Timeout.Value.TotalSeconds, "client-specific settings should always win, even if defaults were changed after");
+ Assert.AreEqual(789, cli3.Settings.Timeout.Value.TotalSeconds, "newer defaults should squash older");
+ }
+
+ [Test]
+ public void can_remove() {
+ // hard to test directly, but if we get a new instance after deleting, safe to assume it works.
+ var cache = new FlurlClientCache();
+ cache.Add("foo");
+
+ var cli1 = cache.Get("foo");
+ var cli2 = cache.Get("foo");
+ cache.Remove("foo");
+ var cli3 = cache.Get("foo");
+
+ Assert.AreSame(cli1, cli2);
+ Assert.AreNotSame(cli1, cli3);
+
+ Assert.IsTrue(cli1.IsDisposed);
+ Assert.IsTrue(cli2.IsDisposed);
+ Assert.IsFalse(cli3.IsDisposed);
+ }
+ }
+}
diff --git a/test/Flurl.Test/Http/FlurlClientFactoryTests.cs b/test/Flurl.Test/Http/FlurlClientFactoryTests.cs
deleted file mode 100644
index ce4de4ef..00000000
--- a/test/Flurl.Test/Http/FlurlClientFactoryTests.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Flurl.Http.Configuration;
-using NUnit.Framework;
-
-namespace Flurl.Test.Http
-{
- [TestFixture]
- public class FlurlClientFactoryTests
- {
- [Test]
- public void default_factory_provides_same_client_per_host_scheme_port() {
- var fac = new DefaultFlurlClientFactory();
- var cli1 = fac.Get("http://api.com/foo");
- var cli2 = fac.Get("http://api.com/bar");
- var cli3 = fac.Get("https://api.com/foo");
- var cli4 = fac.Get("https://api.com/bar");
- var cli5 = fac.Get("https://api.com:1234/foo");
- var cli6 = fac.Get("https://api.com:1234/bar");
-
- Assert.AreSame(cli1, cli2);
- Assert.AreSame(cli3, cli4);
- Assert.AreSame(cli5, cli6);
-
- Assert.AreNotSame(cli1, cli3);
- Assert.AreNotSame(cli3, cli5);
- }
-
- [Test]
- public void per_base_url_factory_provides_same_client_per_provided_url() {
- var fac = new PerBaseUrlFlurlClientFactory();
- var cli1 = fac.Get("http://api.com/foo");
- var cli2 = fac.Get("http://api.com/bar");
- var cli3 = fac.Get("http://api.com/foo");
- Assert.AreNotSame(cli1, cli2);
- Assert.AreSame(cli1, cli3);
- }
-
- [Test]
- public void can_configure_client_from_factory() {
- var fac = new DefaultFlurlClientFactory()
- .ConfigureClient("http://api.com/foo", c => c.Settings.Timeout = TimeSpan.FromSeconds(123));
- Assert.AreEqual(TimeSpan.FromSeconds(123), fac.Get("http://api.com/bar").Settings.Timeout);
- Assert.AreNotEqual(TimeSpan.FromSeconds(123), fac.Get("http://api2.com/foo").Settings.Timeout);
- }
-
- [Test]
- public async Task ConfigureClient_is_thread_safe() {
- var fac = new DefaultFlurlClientFactory();
- var sequence = new List();
-
- var task1 = Task.Run(() => fac.ConfigureClient("http://api.com", c => {
- sequence.Add(1);
- Thread.Sleep(5000);
- sequence.Add(3);
- }));
-
- await Task.Delay(200);
-
- // modifies same client as task1, should get blocked until task1 is done
- var task2 = Task.Run(() => fac.ConfigureClient("http://api.com", c => {
- sequence.Add(4);
- }));
-
- await Task.Delay(200);
-
- // modifies different client, should run immediately
- var task3 = Task.Run(() => fac.ConfigureClient("http://api2.com", c => {
- sequence.Add(2);
- }));
-
- await Task.WhenAll(task1, task2, task3);
- Assert.AreEqual("1,2,3,4", string.Join(",", sequence));
- }
- }
-}
diff --git a/test/Flurl.Test/Http/GlobalConfigTests.cs b/test/Flurl.Test/Http/GlobalConfigTests.cs
new file mode 100644
index 00000000..fecd033b
--- /dev/null
+++ b/test/Flurl.Test/Http/GlobalConfigTests.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Threading.Tasks;
+using Flurl.Http;
+using NUnit.Framework;
+
+namespace Flurl.Test.Http
+{
+ [TestFixture, NonParallelizable]
+ public class GlobalConfigTests : HttpTestFixtureBase
+ {
+ [SetUp, TearDown]
+ public void ResetGlobalDefaults() {
+ FlurlHttp.UseClientPerHostStrategy();
+ FlurlHttp.Clients.Clear();
+ }
+
+ // different path - share client
+ [TestCase("https://api.com/some/path", "https://api.com/other/path", true)]
+ // different userinfo - share client
+ [TestCase("https://api.com", "https://user:pass@api.com", true)]
+ // different host - don't share
+ [TestCase("https://api.com", "https://www.api.com", false)]
+ // different port - don't share
+ [TestCase("https://api.com", "https://api.com:3333", false)]
+ // different scheme - don't share
+ [TestCase("http://api.com", "https://api.com", false)]
+ public void default_caching_strategy_provides_same_client_per_host_scheme_port(string url1, string url2, bool sameClient) {
+ var cli1 = FlurlHttp.GetClientForRequest(new FlurlRequest(url1));
+ var cli2 = FlurlHttp.GetClientForRequest(new FlurlRequest(url2));
+
+ if (sameClient)
+ Assert.AreSame(cli1, cli2);
+ else
+ Assert.AreNotSame(cli1, cli2);
+ }
+
+ [Test]
+ public async Task can_replace_caching_strategy() {
+ FlurlHttp.UseClientCachingStrategy(req => "shared");
+ Assert.AreSame(
+ FlurlHttp.GetClientForRequest(new FlurlRequest("https://a.com")),
+ FlurlHttp.GetClientForRequest(new FlurlRequest("https://b.com")));
+
+ var req1 = new FlurlRequest("https://a.com");
+ await req1.GetAsync();
+ var req2 = new FlurlRequest("https://b.com");
+ await req2.GetAsync();
+
+ Assert.AreSame(req1.Client, req2.Client, "custom strategy uses same client for all calls");
+
+ FlurlHttp.UseClientPerHostStrategy();
+ Assert.AreNotSame(
+ FlurlHttp.GetClientForRequest(new FlurlRequest("https://a.com")),
+ FlurlHttp.GetClientForRequest(new FlurlRequest("https://b.com")));
+
+ req1 = new FlurlRequest("https://a.com");
+ await req1.GetAsync();
+ req2 = new FlurlRequest("https://b.com");
+ await req2.GetAsync();
+
+ Assert.AreNotSame(req1.Client, req2.Client, "default strategy uses different client per host");
+ }
+
+ [Test]
+ public void can_configure_client_for_url() {
+ FlurlHttp.ConfigureClientForUrl("https://api.com/some/path")
+ .WithSettings(s => s.Timeout = TimeSpan.FromSeconds(123));
+
+ var cli1 = FlurlHttp.GetClientForRequest(new FlurlRequest("https://api.com"));
+ var cli2 = FlurlHttp.GetClientForRequest(new FlurlRequest("https://api.com/differernt/path"));
+ var cli3 = FlurlHttp.GetClientForRequest(new FlurlRequest("https://api.com:3333"));
+
+ Assert.AreEqual(123, cli1.Settings.Timeout.Value.TotalSeconds);
+ Assert.AreEqual(123, cli2.Settings.Timeout.Value.TotalSeconds);
+ Assert.AreNotEqual(123, cli3.Settings.Timeout.Value.TotalSeconds);
+ }
+ }
+}
diff --git a/test/Flurl.Test/Http/MultipartTests.cs b/test/Flurl.Test/Http/MultipartTests.cs
index 500d4266..396d6c4e 100644
--- a/test/Flurl.Test/Http/MultipartTests.cs
+++ b/test/Flurl.Test/Http/MultipartTests.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading.Tasks;
using Flurl.Http;
+using Flurl.Http.Configuration;
using Flurl.Http.Content;
using Flurl.Http.Testing;
using NUnit.Framework;
@@ -17,7 +18,7 @@ public class MultipartTests
{
[Test]
public async Task can_build_and_send_multipart_content() {
- var content = new CapturedMultipartContent()
+ var content = new CapturedMultipartContent(new FlurlHttpSettings())
.AddString("string", "foo")
.AddString("string2", "bar", "text/blah")
.AddStringParts(new { part1 = 1, part2 = 2, part3 = (string)null }) // part3 should be excluded
@@ -67,7 +68,7 @@ private void AssertFilePart(HttpContent part, string name, string fileName, stri
[Test]
public void must_provide_required_args_to_builder() {
- var content = new CapturedMultipartContent();
+ var content = new CapturedMultipartContent(new FlurlHttpSettings());
Assert.Throws(() => content.AddStringParts(null));
Assert.Throws(() => content.AddString("other", null));
Assert.Throws(() => content.AddString(null, "hello!"));
diff --git a/test/Flurl.Test/Http/SettingsExtensionsTests.cs b/test/Flurl.Test/Http/SettingsExtensionsTests.cs
index a2340fd0..96cb170e 100644
--- a/test/Flurl.Test/Http/SettingsExtensionsTests.cs
+++ b/test/Flurl.Test/Http/SettingsExtensionsTests.cs
@@ -192,7 +192,7 @@ public void can_use_uri_with_WithUrl() {
[Test]
public void can_override_settings_fluently() {
using (var test = new HttpTest()) {
- var cli = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*");
+ var cli = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*");
test.RespondWith("epic fail", 500);
var req = "http://www.api.com".WithSettings(c => c.AllowedHttpStatusRange = "2xx");
req.Client = cli; // client-level settings shouldn't win
diff --git a/test/Flurl.Test/Http/SettingsTests.cs b/test/Flurl.Test/Http/SettingsTests.cs
index 547892f5..64e2f7cd 100644
--- a/test/Flurl.Test/Http/SettingsTests.cs
+++ b/test/Flurl.Test/Http/SettingsTests.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using System.Net.Http;
using System.Threading.Tasks;
using Flurl.Http;
using Flurl.Http.Configuration;
@@ -10,7 +9,7 @@
namespace Flurl.Test.Http
{
///
- /// FlurlHttpSettings are available at the global, test, client, and request level. This abstract class
+ /// FlurlHttpSettings are available at the test, client, and request level. This abstract class
/// allows the same tests to be run against settings at all 4 levels.
///
public abstract class SettingsTestsBase
@@ -163,100 +162,7 @@ public async Task explicit_content_type_header_is_not_overridden() {
}
}
- [TestFixture, NonParallelizable] // touches global settings, so can't run in parallel
- public class GlobalSettingsTests : SettingsTestsBase
- {
- protected override FlurlHttpSettings GetSettings() => FlurlHttp.GlobalSettings;
- protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com");
-
- [TearDown]
- public void ResetDefaults() => FlurlHttp.GlobalSettings.ResetDefaults();
-
- [Test]
- public void settings_propagate_correctly() {
- FlurlHttp.GlobalSettings.Redirects.Enabled = false;
- FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "4xx";
- FlurlHttp.GlobalSettings.Redirects.MaxAutoRedirects = 123;
-
- var client1 = new FlurlClient();
- client1.Settings.Redirects.Enabled = true;
- Assert.AreEqual("4xx", client1.Settings.AllowedHttpStatusRange);
- Assert.AreEqual(123, client1.Settings.Redirects.MaxAutoRedirects);
- client1.Settings.AllowedHttpStatusRange = "5xx";
- client1.Settings.Redirects.MaxAutoRedirects = 456;
-
- var req = client1.Request("http://myapi.com");
- Assert.IsTrue(req.Settings.Redirects.Enabled, "request should inherit client settings when not set at request level");
- Assert.AreEqual("5xx", req.Settings.AllowedHttpStatusRange, "request should inherit client settings when not set at request level");
- Assert.AreEqual(456, req.Settings.Redirects.MaxAutoRedirects, "request should inherit client settings when not set at request level");
-
- var client2 = new FlurlClient();
- client2.Settings.Redirects.Enabled = false;
-
- req.Client = client2;
- Assert.IsFalse(req.Settings.Redirects.Enabled, "request should inherit client settings when not set at request level");
- Assert.AreEqual("4xx", req.Settings.AllowedHttpStatusRange, "request should inherit global settings when not set at request or client level");
- Assert.AreEqual(123, req.Settings.Redirects.MaxAutoRedirects, "request should inherit global settings when not set at request or client level");
-
- client2.Settings.Redirects.Enabled = true;
- client2.Settings.AllowedHttpStatusRange = "3xx";
- client2.Settings.Redirects.MaxAutoRedirects = 789;
- Assert.IsTrue(req.Settings.Redirects.Enabled, "request should inherit client settings when not set at request level");
- Assert.AreEqual("3xx", req.Settings.AllowedHttpStatusRange, "request should inherit client settings when not set at request level");
- Assert.AreEqual(789, req.Settings.Redirects.MaxAutoRedirects, "request should inherit client settings when not set at request level");
-
- req.Settings.Redirects.Enabled = false;
- req.Settings.AllowedHttpStatusRange = "6xx";
- req.Settings.Redirects.MaxAutoRedirects = 2;
- Assert.IsFalse(req.Settings.Redirects.Enabled, "request-level settings should override any defaults");
- Assert.AreEqual("6xx", req.Settings.AllowedHttpStatusRange, "request-level settings should override any defaults");
- Assert.AreEqual(2, req.Settings.Redirects.MaxAutoRedirects, "request-level settings should override any defaults");
-
- req.Settings.ResetDefaults();
- Assert.IsTrue(req.Settings.Redirects.Enabled, "request should inherit client settings when cleared at request level");
- Assert.AreEqual("3xx", req.Settings.AllowedHttpStatusRange, "request should inherit client settings when cleared request level");
- Assert.AreEqual(789, req.Settings.Redirects.MaxAutoRedirects, "request should inherit client settings when cleared request level");
-
- client2.Settings.ResetDefaults();
- Assert.IsFalse(req.Settings.Redirects.Enabled, "request should inherit global settings when cleared at request and client level");
- Assert.AreEqual("4xx", req.Settings.AllowedHttpStatusRange, "request should inherit global settings when cleared at request and client level");
- Assert.AreEqual(123, req.Settings.Redirects.MaxAutoRedirects, "request should inherit global settings when cleared at request and client level");
- }
-
- [Test]
- public async Task can_provide_custom_client_factory() {
- FlurlHttp.GlobalSettings.FlurlClientFactory = new MyCustomClientFactory();
- var req = GetRequest();
-
- // client not assigned until request is sent
- using var test = new HttpTest();
- await req.GetAsync();
-
- Assert.IsInstanceOf(req.Client.HttpClient);
- }
-
- [Test]
- public void can_configure_global_from_FlurlHttp_object() {
- FlurlHttp.Configure(settings => settings.Redirects.Enabled = false);
- Assert.IsFalse(FlurlHttp.GlobalSettings.Redirects.Enabled);
- }
-
- [Test]
- public async Task can_configure_client_from_FlurlHttp_object() {
- FlurlHttp.ConfigureClient("http://host1.com/foo", cli => cli.Settings.Redirects.Enabled = false);
- var req1 = new FlurlRequest("http://host1.com/bar"); // different URL but same host, should use above client
- var req2 = new FlurlRequest("http://host2.com"); // different host, should use new client
-
- // client not assigned until request is sent
- using var test = new HttpTest();
- await Task.WhenAll(req1.GetAsync(), req2.GetAsync());
-
- Assert.IsFalse(req1.Client.Settings.Redirects.Enabled);
- Assert.IsTrue(req2.Client.Settings.Redirects.Enabled);
- }
- }
-
- [TestFixture, Parallelizable]
+ [TestFixture]
public class HttpTestSettingsTests : SettingsTestsBase
{
private HttpTest _test;
@@ -296,7 +202,7 @@ private class FakeSerializer : ISerializer
}
}
- [TestFixture, Parallelizable]
+ [TestFixture]
public class ClientSettingsTests : SettingsTestsBase
{
private readonly Lazy _client = new Lazy(() => new FlurlClient());
@@ -305,7 +211,7 @@ public class ClientSettingsTests : SettingsTestsBase
protected override IFlurlRequest GetRequest() => _client.Value.Request("http://api.com");
}
- [TestFixture, Parallelizable]
+ [TestFixture]
public class RequestSettingsTests : SettingsTestsBase
{
private readonly Lazy _req = new Lazy(() => new FlurlRequest("http://api.com"));
@@ -314,18 +220,11 @@ public class RequestSettingsTests : SettingsTestsBase
protected override IFlurlRequest GetRequest() => _req.Value;
[Test]
- public void request_gets_global_settings_when_no_client() {
+ public void request_gets_default_settings_when_no_client() {
var req = new FlurlRequest();
Assert.IsNull(req.Client);
Assert.IsNull(req.Url);
- Assert.AreEqual(FlurlHttp.GlobalSettings.JsonSerializer, req.Settings.JsonSerializer);
+ Assert.IsInstanceOf(req.Settings.JsonSerializer);
}
}
-
- class MyCustomClientFactory : DefaultFlurlClientFactory
- {
- public override HttpClient CreateHttpClient(HttpMessageHandler handler) => new MyCustomHttpClient();
- }
-
- class MyCustomHttpClient : HttpClient { }
}
diff --git a/test/Flurl.Test/Http/TestingTests.cs b/test/Flurl.Test/Http/TestingTests.cs
index 616cb168..a1f40f29 100644
--- a/test/Flurl.Test/Http/TestingTests.cs
+++ b/test/Flurl.Test/Http/TestingTests.cs
@@ -439,7 +439,7 @@ public async Task can_deserialize_default_response_more_than_once() {
[Test]
public void can_configure_settings_for_test() {
Assert.IsTrue(new FlurlRequest().Settings.Redirects.Enabled);
- using (new HttpTest().Configure(settings => settings.Redirects.Enabled = false)) {
+ using (new HttpTest().WithSettings(settings => settings.Redirects.Enabled = false)) {
Assert.IsFalse(new FlurlRequest().Settings.Redirects.Enabled);
}
// test disposed, should revert back to global settings