Skip to content

Commit 12d6775

Browse files
askptkylejuliandevCopilot
authored
feat: Add Dependency Injection support (#459)
Signed-off-by: André Silva <[email protected]> Signed-off-by: Kyle Julian <[email protected]> Co-authored-by: Kyle Julian <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 6f9e105 commit 12d6775

12 files changed

+697
-3
lines changed

src/OpenFeature.Providers.Ofrep/Client/OfrepClient.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ public OfrepClient(OfrepOptions configuration, ILogger? logger = null)
4242
{
4343
}
4444

45+
/// <summary>
46+
/// Creates a new instance of <see cref="OfrepClient"/> using a provided <see cref="HttpClient"/>.
47+
/// </summary>
48+
/// <param name="httpClient">The HttpClient to use for requests. Caller may provide one from IHttpClientFactory.</param>
49+
/// <param name="logger">The logger for the client.</param>
50+
internal OfrepClient(HttpClient httpClient, ILogger? logger = null)
51+
{
52+
#if NET8_0_OR_GREATER
53+
ArgumentNullException.ThrowIfNull(httpClient);
54+
#else
55+
if (httpClient == null)
56+
{
57+
throw new ArgumentNullException(nameof(httpClient));
58+
}
59+
#endif
60+
61+
this._logger = logger ?? NullLogger<OfrepClient>.Instance;
62+
this._httpClient = httpClient;
63+
}
64+
4565
/// <summary>
4666
/// Internal constructor for testing purposes.
4767
/// </summary>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Options;
3+
using OpenFeature.DependencyInjection;
4+
using OpenFeature.Providers.Ofrep.Configuration;
5+
#if NETFRAMEWORK
6+
using System.Net.Http;
7+
#endif
8+
using Microsoft.Extensions.Logging;
9+
using OpenFeature.Providers.Ofrep.Client;
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
11+
12+
namespace OpenFeature.Providers.Ofrep.DependencyInjection;
13+
14+
/// <summary>
15+
/// Extension methods for configuring the OpenFeatureBuilder with Ofrep provider.
16+
/// </summary>
17+
public static class FeatureBuilderExtensions
18+
{
19+
/// <summary>
20+
/// Adds the OfrepProvider with configured options.
21+
/// </summary>
22+
public static OpenFeatureBuilder AddOfrepProvider(this OpenFeatureBuilder builder, Action<OfrepProviderOptions> configure)
23+
{
24+
builder.Services.Configure(OfrepProviderOptions.DefaultName, configure);
25+
builder.Services.TryAddSingleton<IValidateOptions<OfrepProviderOptions>, OfrepProviderOptionsValidator>();
26+
return builder.AddProvider(sp => CreateProvider(sp, null));
27+
}
28+
29+
/// <summary>
30+
/// Adds the OfrepProvider for a named domain with configured options.
31+
/// </summary>
32+
public static OpenFeatureBuilder AddOfrepProvider(this OpenFeatureBuilder builder, string domain, Action<OfrepProviderOptions> configure)
33+
{
34+
builder.Services.Configure(domain, configure);
35+
builder.Services.TryAddSingleton<IValidateOptions<OfrepProviderOptions>, OfrepProviderOptionsValidator>();
36+
return builder.AddProvider(domain, CreateProvider);
37+
}
38+
39+
private static OfrepProvider CreateProvider(IServiceProvider sp, string? domain)
40+
{
41+
var monitor = sp.GetRequiredService<IOptionsMonitor<OfrepProviderOptions>>();
42+
var opts = string.IsNullOrWhiteSpace(domain) ? monitor.Get(OfrepProviderOptions.DefaultName) : monitor.Get(domain);
43+
44+
// Options validation is handled by OfrepProviderOptionsValidator during service registration
45+
var ofrepOptions = new OfrepOptions(opts.BaseUrl)
46+
{
47+
Timeout = opts.Timeout,
48+
Headers = opts.Headers
49+
};
50+
51+
// Resolve or create HttpClient if caller wants to manage it
52+
HttpClient? httpClient = null;
53+
54+
// Prefer IHttpClientFactory if available
55+
var factory = sp.GetService<IHttpClientFactory>();
56+
if (factory != null)
57+
{
58+
httpClient = string.IsNullOrWhiteSpace(opts.HttpClientName) ? factory.CreateClient() : factory.CreateClient(opts.HttpClientName!);
59+
}
60+
61+
// If no factory/client, let OfrepClient create its own HttpClient
62+
if (httpClient == null)
63+
{
64+
return new OfrepProvider(ofrepOptions); // internal client management
65+
}
66+
67+
// Allow user to configure the HttpClient
68+
opts.ConfigureHttpClient?.Invoke(sp, httpClient);
69+
70+
// Ensure base address/timeout/headers align with options unless already set by user
71+
if (httpClient.BaseAddress == null)
72+
{
73+
httpClient.BaseAddress = new Uri(ofrepOptions.BaseUrl);
74+
}
75+
httpClient.Timeout = ofrepOptions.Timeout;
76+
foreach (var header in ofrepOptions.Headers)
77+
{
78+
if (!httpClient.DefaultRequestHeaders.Contains(header.Key))
79+
{
80+
httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
81+
}
82+
}
83+
84+
// Build OfrepClient using provided HttpClient and wire into OfrepProvider
85+
var loggerFactory = sp.GetService<ILoggerFactory>();
86+
var logger = loggerFactory?.CreateLogger<OfrepClient>();
87+
var ofrepClient = new OfrepClient(httpClient, logger);
88+
return new OfrepProvider(ofrepClient);
89+
}
90+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#if NETFRAMEWORK
2+
using System.Net.Http;
3+
#endif
4+
5+
namespace OpenFeature.Providers.Ofrep.DependencyInjection;
6+
7+
/// <summary>
8+
/// Configuration options for registering the OfrepProvider via DI.
9+
/// </summary>
10+
public record OfrepProviderOptions
11+
{
12+
/// <summary>
13+
/// Default options name for Ofrep provider registrations.
14+
/// </summary>
15+
public const string DefaultName = "OfrepProvider";
16+
17+
/// <summary>
18+
/// The base URL for the OFREP endpoint. Required.
19+
/// </summary>
20+
public string BaseUrl { get; set; } = string.Empty;
21+
22+
/// <summary>
23+
/// HTTP request timeout. Defaults to 10 seconds.
24+
/// </summary>
25+
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
26+
27+
/// <summary>
28+
/// Optional additional HTTP headers.
29+
/// </summary>
30+
public Dictionary<string, string> Headers { get; set; } = new();
31+
32+
/// <summary>
33+
/// Optional named HttpClient to use via IHttpClientFactory.
34+
/// If set, the provider will resolve an IHttpClientFactory and create the named client.
35+
/// You must register the client in your ServiceCollection using AddHttpClient(name, ...).
36+
/// </summary>
37+
public string? HttpClientName { get; set; }
38+
39+
/// <summary>
40+
/// Optional callback to configure the HttpClient used by the provider.
41+
/// If <see cref="HttpClientName"/> is set, the named client will be resolved first and then this delegate is invoked.
42+
/// If not set, a default client will be created (preferably from IHttpClientFactory if available) and then configured.
43+
/// </summary>
44+
public Action<IServiceProvider, HttpClient>? ConfigureHttpClient { get; set; }
45+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Microsoft.Extensions.Options;
2+
3+
namespace OpenFeature.Providers.Ofrep.DependencyInjection;
4+
5+
/// <summary>
6+
/// Validator for OfrepProviderOptions to ensure required fields are set during service registration.
7+
/// </summary>
8+
internal class OfrepProviderOptionsValidator : IValidateOptions<OfrepProviderOptions>
9+
{
10+
public ValidateOptionsResult Validate(string? name, OfrepProviderOptions options)
11+
{
12+
if (string.IsNullOrWhiteSpace(options.BaseUrl))
13+
{
14+
return ValidateOptionsResult.Fail("Ofrep BaseUrl is required. Set it on OfrepProviderOptions.BaseUrl.");
15+
}
16+
17+
// Validate that it's a valid absolute URI
18+
if (!Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out var uri))
19+
{
20+
return ValidateOptionsResult.Fail("Ofrep BaseUrl must be a valid absolute URI.");
21+
}
22+
23+
// Validate that it uses HTTP or HTTPS scheme (required for OFREP)
24+
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
25+
{
26+
return ValidateOptionsResult.Fail("Ofrep BaseUrl must use HTTP or HTTPS scheme.");
27+
}
28+
29+
return ValidateOptionsResult.Success;
30+
}
31+
}

src/OpenFeature.Providers.Ofrep/OfrepProvider.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ public OfrepProvider(OfrepOptions configuration)
3131
this._client = new OfrepClient(configuration);
3232
}
3333

34+
/// <summary>
35+
/// Creates new instance of <see cref="OfrepProvider"/> with a pre-constructed client.
36+
/// </summary>
37+
/// <param name="client">The OFREP client.</param>
38+
internal OfrepProvider(IOfrepClient client)
39+
{
40+
this._client = client ?? throw new ArgumentNullException(nameof(client));
41+
}
42+
3443
/// <inheritdoc/>
3544
public override Task ShutdownAsync(CancellationToken cancellationToken = default)
3645
{

src/OpenFeature.Providers.Ofrep/OpenFeature.Providers.Ofrep.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
<ItemGroup>
2323
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0"/>
24-
<PackageReference Include="OpenFeature" Version="$(OpenFeatureVersion)"/>
24+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/>
25+
<PackageReference Include="OpenFeature.DependencyInjection" Version="$(OpenFeatureVersion)"/>
2526
</ItemGroup>
2627
<PropertyGroup>
2728
<OpenFeatureVersion>[2.2,2.99999]</OpenFeatureVersion>

test/OpenFeature.Providers.Ofrep.Test/Client/OfrepClientTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public void Constructor_WithValidConfiguration_ShouldInitializeSuccessfully()
5959
public void Constructor_WithNullConfiguration_ShouldThrowArgumentNullException()
6060
{
6161
// Arrange, Act & Assert
62-
Assert.Throws<ArgumentNullException>(() => new OfrepClient(null!, this._mockLogger));
62+
Assert.Throws<ArgumentNullException>(() => new OfrepClient((OfrepOptions)null!, this._mockLogger));
6363
}
6464

6565
[Fact]

0 commit comments

Comments
 (0)