Skip to content
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
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -19,39 +20,41 @@ public static class HttpClientWithCacheExtensions
/// <param name="cacheOptions">Cache options including default headers and settings (optional)</param>
/// <returns>Service collection for method chaining</returns>
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<ISimpleCacheKeyGenerator, DefaultSimpleCacheKeyGenerator>();

// Register the universal HTTP client with cache
services.AddSingleton<IHttpClientWithCache>(serviceProvider =>
{
IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
System.Net.Http.HttpClient httpClient = httpClientName is null or ""
? httpClientFactory.CreateClient()
: httpClientFactory.CreateClient(httpClientName);

IMemoryCache cache = serviceProvider.GetRequiredService<IMemoryCache>();
IHttpResponseHandler responseHandler = serviceProvider.GetRequiredService<IHttpResponseHandler>(); // Use universal handler
ISimpleCacheKeyGenerator cacheKeyGenerator = serviceProvider.GetRequiredService<ISimpleCacheKeyGenerator>();
ILogger<HttpClientWithCache>? logger = serviceProvider.GetService<ILogger<HttpClientWithCache>>();

return new HttpClientWithCache(
httpClient,
cache,
responseHandler,
cacheOptions,
cacheKeyGenerator,
logger);
});
// Register the universal HTTP client with cache as scoped to avoid captive dependency
services.AddScoped<IHttpClientWithCache>(sp => CreateHttpClientWithCache(sp, httpClientName, cacheOptions));
services.AddScoped(sp => (HttpClientWithCache)sp.GetRequiredService<IHttpClientWithCache>());

return services;
}

/// <summary>
/// Adds universal HTTP client with caching to the service collection
/// </summary>
/// <param name="services">Service collection</param>
/// <param name="httpClientName">HTTP client name (optional)</param>
/// <param name="configureCacheOptions">Action to configure cache options
/// which then will be registered as <see cref="IOptions{TOptions}"/> named after <paramref name="httpClientName"/></param>
/// <returns>Service collection for method chaining</returns>
public static IServiceCollection AddHttpClientWithCache(
this IServiceCollection services,
string? httpClientName,
Action<HttpCacheOptions> configureCacheOptions)
{
var options = new HttpCacheOptions();
configureCacheOptions(options);
services.Configure(httpClientName, configureCacheOptions);

return services.AddHttpClientWithCache(httpClientName, options);
}

/// <summary>
/// Adds universal HTTP client with caching and resilience to the service collection
/// </summary>
Expand Down Expand Up @@ -97,4 +100,86 @@ public static IServiceCollection AddResilientHttpClientWithCache(
// Add universal HTTP client with cache
return services.AddHttpClientWithCache(httpClientName, cacheOptions);
}

/// <summary>
/// Adds universal HTTP client with caching and resilience to the service collection
/// </summary>
/// <param name="services">Service collection</param>
/// <param name="httpClientName">HTTP client name</param>
/// <param name="configureResilience">Action to configure resilience options</param>
/// <param name="configureCacheOptions">Action to configure cache options</param>
/// <returns>Service collection for method chaining</returns>
public static IServiceCollection AddResilientHttpClientWithCache(
this IServiceCollection services,
string httpClientName,
Action<HttpCacheOptions> configureCacheOptions,
Action<HttpClientOptions>? configureResilience = null)
{
var cacheOptions = new HttpCacheOptions();
configureCacheOptions(cacheOptions);
services.Configure(httpClientName, configureCacheOptions);

return services.AddResilientHttpClientWithCache(httpClientName, configureResilience, cacheOptions);
}

/// <summary>
/// Adds universal HTTP client with caching using preset resilience configuration
/// </summary>
/// <param name="services">Service collection</param>
/// <param name="httpClientName">HTTP client name</param>
/// <param name="preset">Predefined resilience preset</param>
/// <param name="customizeOptions">Optional action to customize preset options</param>
/// <param name="configureCacheOptions">Action to configure cache options</param>
/// <returns>Service collection for method chaining</returns>
public static IServiceCollection AddResilientHttpClientWithCache(
this IServiceCollection services,
string httpClientName,
HttpClientOptions preset,
Action<HttpCacheOptions> configureCacheOptions,
Action<HttpClientOptions>? customizeOptions = null)
{
var cacheOptions = new HttpCacheOptions();
configureCacheOptions(cacheOptions);
services.Configure(httpClientName, configureCacheOptions);

return services.AddResilientHttpClientWithCache(httpClientName, preset, customizeOptions, cacheOptions);
}

/// <summary>
/// Creates an instance of <see cref="HttpClientWithCache"/> using the provided service provider and configuration.
/// </summary>
/// <param name="serviceProvider">The service provider used to resolve required dependencies.</param>
/// <param name="httpClientName">The name of the HTTP client to retrieve from the <see cref="IHttpClientFactory"/>.
/// <param name="cacheOptions">Cache options including default headers and settings (optional)</param>
/// If null or empty, a default client is created.</param>
/// <returns>A configured instance of <see cref="HttpClientWithCache"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if a required service (e.g., <see cref="IHttpClientFactory"/>, <see cref="IMemoryCache"/>,
/// <see cref="IHttpResponseHandler"/>, or <see cref="ISimpleCacheKeyGenerator"/>) is not registered in the service provider.</exception>
private static HttpClientWithCache CreateHttpClientWithCache(
IServiceProvider serviceProvider,
string? httpClientName,
HttpCacheOptions? cacheOptions)
{
IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
System.Net.Http.HttpClient httpClient = httpClientName is null or ""
? httpClientFactory.CreateClient()
: httpClientFactory.CreateClient(httpClientName);

IMemoryCache cache = serviceProvider.GetRequiredService<IMemoryCache>();
IHttpResponseHandler responseHandler = serviceProvider.GetRequiredService<IHttpResponseHandler>();
HttpCacheOptions? cacheOptionsToInject = cacheOptions is null
? serviceProvider.GetService<IOptionsSnapshot<HttpCacheOptions>>()?.Get(httpClientName)
: cacheOptions;
IOptionsSnapshot<HttpCacheOptions>? cacheOptionsSnapshot = serviceProvider.GetService<IOptionsSnapshot<HttpCacheOptions>>();
ISimpleCacheKeyGenerator cacheKeyGenerator = serviceProvider.GetRequiredService<ISimpleCacheKeyGenerator>();
ILogger<HttpClientWithCache>? logger = serviceProvider.GetService<ILogger<HttpClientWithCache>>();

return new HttpClientWithCache(
httpClient,
cache,
responseHandler,
cacheOptionsToInject,
cacheKeyGenerator,
logger);
}
}
Original file line number Diff line number Diff line change
@@ -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<IHttpResponseHandler, DefaultHttpResponseHandler>();

// Act
services.AddHttpClientWithCache("CachedClient");
ServiceProvider serviceProvider = services.BuildServiceProvider();

// Assert
serviceProvider.GetService<IHttpClientWithCache>().Should().NotBeNull().And.BeOfType<HttpClientWithCache>();
serviceProvider.GetService<IMemoryCache>().Should().NotBeNull();
serviceProvider.GetService<ISimpleCacheKeyGenerator>().Should().NotBeNull();
serviceProvider.GetService<System.Net.Http.HttpClient>().Should().NotBeNull();
}

[Fact]
public void AddHttpClientWithCache_WithNamedClient_RegistersNamedHttpClient()
{
// Arrange
const string clientName = "CachedClient";
var services = new ServiceCollection();
services.AddLogging();
services.AddHttpClient();
services.AddSingleton<IHttpResponseHandler, DefaultHttpResponseHandler>();

// Act
services.AddHttpClientWithCache(clientName);
ServiceProvider serviceProvider = services.BuildServiceProvider();

// Assert
IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
System.Net.Http.HttpClient httpClient = httpClientFactory.CreateClient(clientName);
httpClient.Should().NotBeNull();

IHttpClientWithCache httpClientWithCache = serviceProvider.GetRequiredService<IHttpClientWithCache>();
httpClientWithCache.Should().NotBeNull().And.BeOfType<HttpClientWithCache>();
}

[Fact]
public void AddHttpClientWithCache_WithCacheOptions_AppliesCacheOptions()
{
// Arrange
const string clientName = "CachedClient";
var services = new ServiceCollection();
services.AddLogging();
services.AddHttpClient();
services.AddSingleton<IHttpResponseHandler, DefaultHttpResponseHandler>();

// Act
services.AddHttpClientWithCache(clientName, options => options.DefaultExpiry = TimeSpan.FromMinutes(10));
ServiceProvider serviceProvider = services.BuildServiceProvider();

// Assert
var httpClientWithCache = serviceProvider.GetRequiredService<IHttpClientWithCache>() as HttpClientWithCache;
httpClientWithCache.Should().NotBeNull();

// Ensure that HttpCacheOptions configured to MediumTerm cache preset
// Using IOptionsSnapshot to get settings for particular named HTTP client
IOptionsSnapshot<HttpCacheOptions> optionsSnapshot = serviceProvider.GetRequiredService<IOptionsSnapshot<HttpCacheOptions>>();
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<IHttpResponseHandler, DefaultHttpResponseHandler>();

// Act
services.AddResilientHttpClientWithCache(clientName);
ServiceProvider serviceProvider = services.BuildServiceProvider();

// Assert
IHttpClientFactory httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
System.Net.Http.HttpClient httpClient = httpClientFactory.CreateClient(clientName);
httpClient.Should().NotBeNull();

serviceProvider.GetService<IHttpClientWithCache>().Should().NotBeNull().And.BeOfType<HttpClientWithCache>();
serviceProvider.GetService<IMemoryCache>().Should().NotBeNull();
serviceProvider.GetService<ISimpleCacheKeyGenerator>().Should().NotBeNull();
}

[Fact]
public void AddHttpClientWithCache_MultipleCalls_DoesNotThrow()
{
// Arrange
const string clientName = "CachedClient";
var services = new ServiceCollection();
services.AddLogging();
services.AddHttpClient();
services.AddSingleton<IHttpResponseHandler, DefaultHttpResponseHandler>();

// Act & Assert
Func<IServiceCollection> 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<IHttpResponseHandler, DefaultHttpResponseHandler>();

// Act & Assert
Func<IServiceCollection> 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<IHttpResponseHandler, DefaultHttpResponseHandler>();
services.AddHttpClientWithCache("TestClient");

ServiceProvider serviceProvider = services.BuildServiceProvider();

// Act
HttpClientWithCache? concrete = serviceProvider.GetService<HttpClientWithCache>();
IHttpClientWithCache? abstraction = serviceProvider.GetService<IHttpClientWithCache>();

// Assert - This was the core issue from #5
concrete.Should().NotBeNull();
abstraction.Should().NotBeNull();
ReferenceEquals(concrete, abstraction).Should().BeTrue();
}
}