diff --git a/src/Reliable.HttpClient.Caching/Extensions/HttpClientWithCacheExtensions.cs b/src/Reliable.HttpClient.Caching/Extensions/HttpClientWithCacheExtensions.cs
index 5cfd8cc..8ed085b 100644
--- a/src/Reliable.HttpClient.Caching/Extensions/HttpClientWithCacheExtensions.cs
+++ b/src/Reliable.HttpClient.Caching/Extensions/HttpClientWithCacheExtensions.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Reliable.HttpClient.Caching.Abstractions;
@@ -19,39 +20,41 @@ public static class HttpClientWithCacheExtensions
/// Cache options including default headers and settings (optional)
/// Service collection for method chaining
public static IServiceCollection AddHttpClientWithCache(
- this IServiceCollection services,
- string? httpClientName = null,
- HttpCacheOptions? cacheOptions = null)
+ this IServiceCollection services,
+ string? httpClientName = null,
+ HttpCacheOptions? cacheOptions = null)
{
// Register dependencies
services.AddMemoryCache();
services.AddSingleton();
- // Register the universal HTTP client with cache
- services.AddSingleton(serviceProvider =>
- {
- IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService();
- System.Net.Http.HttpClient httpClient = httpClientName is null or ""
- ? httpClientFactory.CreateClient()
- : httpClientFactory.CreateClient(httpClientName);
-
- IMemoryCache cache = serviceProvider.GetRequiredService();
- IHttpResponseHandler responseHandler = serviceProvider.GetRequiredService(); // Use universal handler
- ISimpleCacheKeyGenerator cacheKeyGenerator = serviceProvider.GetRequiredService();
- ILogger? logger = serviceProvider.GetService>();
-
- return new HttpClientWithCache(
- httpClient,
- cache,
- responseHandler,
- cacheOptions,
- cacheKeyGenerator,
- logger);
- });
+ // Register the universal HTTP client with cache as scoped to avoid captive dependency
+ services.AddScoped(sp => CreateHttpClientWithCache(sp, httpClientName, cacheOptions));
+ services.AddScoped(sp => (HttpClientWithCache)sp.GetRequiredService());
return services;
}
+ ///
+ /// Adds universal HTTP client with caching to the service collection
+ ///
+ /// Service collection
+ /// HTTP client name (optional)
+ /// Action to configure cache options
+ /// which then will be registered as named after
+ /// Service collection for method chaining
+ public static IServiceCollection AddHttpClientWithCache(
+ this IServiceCollection services,
+ string? httpClientName,
+ Action configureCacheOptions)
+ {
+ var options = new HttpCacheOptions();
+ configureCacheOptions(options);
+ services.Configure(httpClientName, configureCacheOptions);
+
+ return services.AddHttpClientWithCache(httpClientName, options);
+ }
+
///
/// Adds universal HTTP client with caching and resilience to the service collection
///
@@ -97,4 +100,86 @@ public static IServiceCollection AddResilientHttpClientWithCache(
// Add universal HTTP client with cache
return services.AddHttpClientWithCache(httpClientName, cacheOptions);
}
+
+ ///
+ /// Adds universal HTTP client with caching and resilience to the service collection
+ ///
+ /// Service collection
+ /// HTTP client name
+ /// Action to configure resilience options
+ /// Action to configure cache options
+ /// Service collection for method chaining
+ public static IServiceCollection AddResilientHttpClientWithCache(
+ this IServiceCollection services,
+ string httpClientName,
+ Action configureCacheOptions,
+ Action? configureResilience = null)
+ {
+ var cacheOptions = new HttpCacheOptions();
+ configureCacheOptions(cacheOptions);
+ services.Configure(httpClientName, configureCacheOptions);
+
+ return services.AddResilientHttpClientWithCache(httpClientName, configureResilience, cacheOptions);
+ }
+
+ ///
+ /// Adds universal HTTP client with caching using preset resilience configuration
+ ///
+ /// Service collection
+ /// HTTP client name
+ /// Predefined resilience preset
+ /// Optional action to customize preset options
+ /// Action to configure cache options
+ /// Service collection for method chaining
+ public static IServiceCollection AddResilientHttpClientWithCache(
+ this IServiceCollection services,
+ string httpClientName,
+ HttpClientOptions preset,
+ Action configureCacheOptions,
+ Action? customizeOptions = null)
+ {
+ var cacheOptions = new HttpCacheOptions();
+ configureCacheOptions(cacheOptions);
+ services.Configure(httpClientName, configureCacheOptions);
+
+ return services.AddResilientHttpClientWithCache(httpClientName, preset, customizeOptions, cacheOptions);
+ }
+
+ ///
+ /// Creates an instance of using the provided service provider and configuration.
+ ///
+ /// The service provider used to resolve required dependencies.
+ /// The name of the HTTP client to retrieve from the .
+ /// Cache options including default headers and settings (optional)
+ /// If null or empty, a default client is created.
+ /// A configured instance of .
+ /// Thrown if a required service (e.g., , ,
+ /// , or ) is not registered in the service provider.
+ private static HttpClientWithCache CreateHttpClientWithCache(
+ IServiceProvider serviceProvider,
+ string? httpClientName,
+ HttpCacheOptions? cacheOptions)
+ {
+ IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService();
+ System.Net.Http.HttpClient httpClient = httpClientName is null or ""
+ ? httpClientFactory.CreateClient()
+ : httpClientFactory.CreateClient(httpClientName);
+
+ IMemoryCache cache = serviceProvider.GetRequiredService();
+ IHttpResponseHandler responseHandler = serviceProvider.GetRequiredService();
+ HttpCacheOptions? cacheOptionsToInject = cacheOptions is null
+ ? serviceProvider.GetService>()?.Get(httpClientName)
+ : cacheOptions;
+ IOptionsSnapshot? cacheOptionsSnapshot = serviceProvider.GetService>();
+ ISimpleCacheKeyGenerator cacheKeyGenerator = serviceProvider.GetRequiredService();
+ ILogger? logger = serviceProvider.GetService>();
+
+ return new HttpClientWithCache(
+ httpClient,
+ cache,
+ responseHandler,
+ cacheOptionsToInject,
+ cacheKeyGenerator,
+ logger);
+ }
}
diff --git a/tests/Reliable.HttpClient.Caching.Tests/HttpClientWithCacheExtensionsTests.cs b/tests/Reliable.HttpClient.Caching.Tests/HttpClientWithCacheExtensionsTests.cs
new file mode 100644
index 0000000..a28abb4
--- /dev/null
+++ b/tests/Reliable.HttpClient.Caching.Tests/HttpClientWithCacheExtensionsTests.cs
@@ -0,0 +1,163 @@
+using FluentAssertions;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+using Reliable.HttpClient.Caching.Abstractions;
+using Reliable.HttpClient.Caching.Extensions;
+
+namespace Reliable.HttpClient.Caching.Tests;
+
+public class HttpClientWithCacheExtensionsTests
+{
+ [Fact]
+ public void AddHttpClientWithCache_RegistersAllRequiredServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddHttpClient();
+ services.AddSingleton();
+
+ // Act
+ services.AddHttpClientWithCache("CachedClient");
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ serviceProvider.GetService().Should().NotBeNull().And.BeOfType();
+ serviceProvider.GetService().Should().NotBeNull();
+ serviceProvider.GetService().Should().NotBeNull();
+ serviceProvider.GetService().Should().NotBeNull();
+ }
+
+ [Fact]
+ public void AddHttpClientWithCache_WithNamedClient_RegistersNamedHttpClient()
+ {
+ // Arrange
+ const string clientName = "CachedClient";
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddHttpClient();
+ services.AddSingleton();
+
+ // Act
+ services.AddHttpClientWithCache(clientName);
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService();
+ System.Net.Http.HttpClient httpClient = httpClientFactory.CreateClient(clientName);
+ httpClient.Should().NotBeNull();
+
+ IHttpClientWithCache httpClientWithCache = serviceProvider.GetRequiredService();
+ httpClientWithCache.Should().NotBeNull().And.BeOfType();
+ }
+
+ [Fact]
+ public void AddHttpClientWithCache_WithCacheOptions_AppliesCacheOptions()
+ {
+ // Arrange
+ const string clientName = "CachedClient";
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddHttpClient();
+ services.AddSingleton();
+
+ // Act
+ services.AddHttpClientWithCache(clientName, options => options.DefaultExpiry = TimeSpan.FromMinutes(10));
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ var httpClientWithCache = serviceProvider.GetRequiredService() as HttpClientWithCache;
+ httpClientWithCache.Should().NotBeNull();
+
+ // Ensure that HttpCacheOptions configured to MediumTerm cache preset
+ // Using IOptionsSnapshot to get settings for particular named HTTP client
+ IOptionsSnapshot optionsSnapshot = serviceProvider.GetRequiredService>();
+ HttpCacheOptions registeredOptions = optionsSnapshot.Get(clientName);
+
+ registeredOptions.DefaultExpiry.Should().Be(TimeSpan.FromMinutes(10));
+ }
+
+ [Fact]
+ public void AddResilientHttpClientWithCache_RegistersAllRequiredServices()
+ {
+ // Arrange
+ const string clientName = "ResilientClient";
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddHttpClient();
+ services.AddSingleton();
+
+ // Act
+ services.AddResilientHttpClientWithCache(clientName);
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService();
+ System.Net.Http.HttpClient httpClient = httpClientFactory.CreateClient(clientName);
+ httpClient.Should().NotBeNull();
+
+ serviceProvider.GetService().Should().NotBeNull().And.BeOfType();
+ serviceProvider.GetService().Should().NotBeNull();
+ serviceProvider.GetService().Should().NotBeNull();
+ }
+
+ [Fact]
+ public void AddHttpClientWithCache_MultipleCalls_DoesNotThrow()
+ {
+ // Arrange
+ const string clientName = "CachedClient";
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddHttpClient();
+ services.AddSingleton();
+
+ // Act & Assert
+ Func act = () => services
+ .AddHttpClientWithCache(clientName)
+ .AddHttpClientWithCache(clientName);
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void AddResilientHttpClientWithCache_MultipleCalls_DoesNotThrow()
+ {
+ // Arrange
+ const string clientName = "ResilientClient";
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddSingleton();
+
+ // Act & Assert
+ Func act = () => services
+ .AddResilientHttpClientWithCache(clientName)
+ .AddResilientHttpClientWithCache(clientName);
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void AddHttpClientWithCache_CanResolveConcreteType_SameInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddHttpClient();
+ services.AddSingleton();
+ services.AddHttpClientWithCache("TestClient");
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ // Act
+ HttpClientWithCache? concrete = serviceProvider.GetService();
+ IHttpClientWithCache? abstraction = serviceProvider.GetService();
+
+ // Assert - This was the core issue from #5
+ concrete.Should().NotBeNull();
+ abstraction.Should().NotBeNull();
+ ReferenceEquals(concrete, abstraction).Should().BeTrue();
+ }
+}