Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2c42d1f
Add OpenFeature.DependencyInjection.Abstractions project
arttonoyan Sep 29, 2025
4d69c6f
Refactor OpenFeature for DI and provider abstraction
arttonoyan Oct 4, 2025
7e26ba1
Refactor AddProvider API for simplicity and consistency
arttonoyan Oct 4, 2025
1edb0c9
Refactor FeatureLifecycleManager initialization
arttonoyan Oct 4, 2025
6cfaeb7
Refactor tests to use OpenFeatureProviderOptions
arttonoyan Oct 4, 2025
dbdb9b5
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Oct 5, 2025
acade51
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Oct 9, 2025
10203f3
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Oct 20, 2025
3d680f3
Removev ID property
arttonoyan Oct 27, 2025
f9a6781
Merge branch 'feature/di-abstractions-builder-api-587' of https://git…
arttonoyan Oct 27, 2025
4b1237a
Update src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
arttonoyan Nov 5, 2025
04ea063
Update src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
arttonoyan Nov 5, 2025
f521321
Refactor OpenFeatureOptions to OpenFeatureProviderOptions
arttonoyan Nov 5, 2025
08cdb69
Refactor Dependency Injection Abstractions
arttonoyan Nov 6, 2025
9884603
chore: Merge dev into current branch and resolve merge conflicts
arttonoyan Nov 6, 2025
e5e9af6
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Nov 6, 2025
e5bc0ca
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Nov 6, 2025
d0de7ee
Remove inheritdoc from FeatureLifecycleManager
arttonoyan Nov 6, 2025
0d7e439
Merge branch 'feature/di-abstractions-builder-api-587' of https://git…
arttonoyan Nov 6, 2025
e842599
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Nov 8, 2025
062df8b
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Nov 8, 2025
b7f6c76
Clean up unused namespaces in test files
arttonoyan Nov 8, 2025
579ac49
Update release.yml
arttonoyan Nov 11, 2025
d34a366
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Nov 11, 2025
3326469
Merge branch 'main' into feature/di-abstractions-builder-api-587
arttonoyan Nov 12, 2025
0843aac
Add cancellation checks and null safety in providers
arttonoyan Nov 13, 2025
0a03843
Cast AddProvider return type to OpenFeatureBuilder
arttonoyan Nov 13, 2025
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
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,11 @@ jobs:
github-token: ${{secrets.GITHUB_TOKEN}}
project-name: OpenFeature.Providers.MultiProvider
release-tag: ${{ needs.release-please.outputs.release_tag_name }}

# Process OpenFeature.Providers.DependencyInjection project
- name: Generate and Attest SBOM for OpenFeature.Providers.DependencyInjection
uses: ./.github/actions/sbom-generator
with:
github-token: ${{secrets.GITHUB_TOKEN}}
project-name: OpenFeature.Providers.DependencyInjection
release-tag: ${{ needs.release-please.outputs.release_tag_name }}
7 changes: 4 additions & 3 deletions OpenFeature.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@
<Project Path="samples/AspNetCore/Samples.AspNetCore.csproj" />
</Folder>
<Folder Name="/src/">
<File Path="src/Directory.Build.props" />
<File Path="src/Directory.Build.targets" />
<Project Path="src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj" />
<Project Path="src/OpenFeature.Hosting/OpenFeature.Hosting.csproj" />
<Project Path="src/OpenFeature.Providers.DependencyInjection/OpenFeature.Providers.DependencyInjection.csproj" />
<Project Path="src/OpenFeature.Providers.MultiProvider/OpenFeature.Providers.MultiProvider.csproj" />
<Project Path="src/OpenFeature/OpenFeature.csproj" />
<File Path="src/Directory.Build.props" />
<File Path="src/Directory.Build.targets" />
</Folder>
<Folder Name="/test/">
<File Path="test/Directory.Build.props" />
<Project Path="test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj" />
<Project Path="test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj" />
<Project Path="test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj" />
Expand All @@ -64,6 +66,5 @@
<Project Path="test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj" />
<Project Path="test/OpenFeature.Providers.MultiProvider.Tests/OpenFeature.Providers.MultiProvider.Tests.csproj" />
<Project Path="test/OpenFeature.Tests/OpenFeature.Tests.csproj" />
<File Path="test/Directory.Build.props" />
</Folder>
</Solution>
27 changes: 24 additions & 3 deletions src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenFeature.Providers.DependencyInjection;

namespace OpenFeature.Hosting.Internal;

Expand All @@ -17,12 +18,24 @@ public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider,
_logger = logger;
}

/// <inheritdoc />
public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default)
{
this.LogStartingInitializationOfFeatureProvider();

var options = _serviceProvider.GetRequiredService<IOptions<OpenFeatureOptions>>().Value;
await InitializeProvidersAsync(cancellationToken).ConfigureAwait(false);
InitializeHooks();
InitializeHandlers();
}

private async Task InitializeProvidersAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var options = _serviceProvider
.GetRequiredService<IOptions<OpenFeatureProviderOptions>>()
.Value
?? throw new InvalidOperationException($"{nameof(OpenFeatureProviderOptions)} are not configured.");

if (options.HasDefaultProvider)
{
var featureProvider = _serviceProvider.GetRequiredService<FeatureProvider>();
Expand All @@ -31,10 +44,16 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke

foreach (var name in options.ProviderNames)
{
cancellationToken.ThrowIfCancellationRequested();

var featureProvider = _serviceProvider.GetRequiredKeyedService<FeatureProvider>(name);
await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false);
}
}

private void InitializeHooks()
{
var options = _serviceProvider.GetRequiredService<IOptions<OpenFeatureOptions>>().Value;
var hooks = new List<Hook>();
foreach (var hookName in options.HookNames)
{
Expand All @@ -43,15 +62,17 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke
}

_featureApi.AddHooks(hooks);
}

private void InitializeHandlers()
{
var handlers = _serviceProvider.GetServices<EventHandlerDelegateWrapper>();
foreach (var handler in handlers)
{
_featureApi.AddHandler(handler.ProviderEventType, handler.EventHandlerDelegate);
}
}

/// <inheritdoc />
public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default)
{
this.LogShuttingDownFeatureProvider();
Expand Down
1 change: 1 addition & 0 deletions src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\OpenFeature.Providers.DependencyInjection\OpenFeature.Providers.DependencyInjection.csproj" />
<ProjectReference Include="..\OpenFeature\OpenFeature.csproj" />
</ItemGroup>

Expand Down
49 changes: 8 additions & 41 deletions src/OpenFeature.Hosting/OpenFeatureBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using OpenFeature.Providers.DependencyInjection;

namespace OpenFeature.Hosting;

/// <summary>
/// Describes a <see cref="OpenFeatureBuilder"/> backed by an <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The services being configured.</param>
public class OpenFeatureBuilder(IServiceCollection services)
public class OpenFeatureBuilder(IServiceCollection services) : OpenFeatureProviderBuilder(services)
{
/// <summary> The services being configured. </summary>
public IServiceCollection Services { get; } = services;

/// <summary>
/// Indicates whether the evaluation context has been configured.
/// This property is used to determine if specific configurations or services
Expand All @@ -19,42 +17,11 @@ public class OpenFeatureBuilder(IServiceCollection services)
public bool IsContextConfigured { get; internal set; }

/// <summary>
/// Indicates whether the policy has been configured.
/// </summary>
public bool IsPolicyConfigured { get; internal set; }

/// <summary>
/// Gets a value indicating whether a default provider has been registered.
/// Adds an <see cref="IFeatureClient"/> to the container, optionally keyed by <paramref name="name"/>.
/// If an evaluation context is configured, the client is created with that context.
/// </summary>
public bool HasDefaultProvider { get; internal set; }

/// <summary>
/// Gets the count of domain-bound providers that have been registered.
/// This count does not include the default provider.
/// </summary>
public int DomainBoundProviderRegistrationCount { get; internal set; }

/// <summary>
/// Validates the current configuration, ensuring that a policy is set when multiple providers are registered
/// or when a default provider is registered alongside another provider.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if multiple providers are registered without a policy, or if both a default provider
/// and an additional provider are registered without a policy configuration.
/// </exception>
public void Validate()
{
if (!IsPolicyConfigured)
{
if (DomainBoundProviderRegistrationCount > 1)
{
throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured.");
}

if (HasDefaultProvider && DomainBoundProviderRegistrationCount == 1)
{
throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration.");
}
}
}
/// <param name="name">Optional key for a named client registration.</param>
/// <returns>The current <see cref="OpenFeatureProviderBuilder"/>.</returns>
protected override OpenFeatureProviderBuilder TryAddClient(string? name = null)
=> this.AddClient(name);
}
132 changes: 1 addition & 131 deletions src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using OpenFeature.Hosting;
using OpenFeature.Hosting.Internal;
using OpenFeature.Model;
using OpenFeature.Providers.DependencyInjection;

namespace OpenFeature;

Expand Down Expand Up @@ -51,108 +52,6 @@ public static OpenFeatureBuilder AddContext(this OpenFeatureBuilder builder, Act
return builder;
}

/// <summary>
/// Adds a feature provider using a factory method without additional configuration options.
/// This method adds the feature provider as a transient service and sets it as the default provider within the application.
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> used to configure feature flags.</param>
/// <param name="implementationFactory">
/// A factory method that creates and returns a <see cref="FeatureProvider"/>
/// instance based on the provided service provider.
/// </param>
/// <returns>The updated <see cref="OpenFeatureBuilder"/> instance with the default feature provider set and configured.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="builder"/> is null, as a valid builder is required to add and configure providers.</exception>
public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func<IServiceProvider, FeatureProvider> implementationFactory)
=> AddProvider<OpenFeatureOptions>(builder, implementationFactory, null);

/// <summary>
/// Adds a feature provider using a factory method to create the provider instance and optionally configures its settings.
/// This method adds the feature provider as a transient service and sets it as the default provider within the application.
/// </summary>
/// <typeparam name="TOptions"> Type derived from <see cref="OpenFeatureOptions"/> used to configure the feature provider.</typeparam>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> used to configure feature flags.</param>
/// <param name="implementationFactory">
/// A factory method that creates and returns a <see cref="FeatureProvider"/>
/// instance based on the provided service provider.
/// </param>
/// <param name="configureOptions">An optional delegate to configure the provider-specific options.</param>
/// <returns>The updated <see cref="OpenFeatureBuilder"/> instance with the default feature provider set and configured.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="builder"/> is null, as a valid builder is required to add and configure providers.</exception>
public static OpenFeatureBuilder AddProvider<TOptions>(this OpenFeatureBuilder builder, Func<IServiceProvider, FeatureProvider> implementationFactory, Action<TOptions>? configureOptions)
where TOptions : OpenFeatureOptions
{
Guard.ThrowIfNull(builder);

builder.HasDefaultProvider = true;
builder.Services.PostConfigure<TOptions>(options => options.AddDefaultProviderName());
if (configureOptions != null)
{
builder.Services.Configure(configureOptions);
}

builder.Services.TryAddTransient(implementationFactory);
builder.AddClient();
return builder;
}

/// <summary>
/// Adds a feature provider for a specific domain using provided options and a configuration builder.
/// </summary>
/// <typeparam name="TOptions"> Type derived from <see cref="OpenFeatureOptions"/> used to configure the feature provider.</typeparam>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> used to configure feature flags.</param>
/// <param name="domain">The unique name of the provider.</param>
/// <param name="implementationFactory">
/// A factory method that creates a feature provider instance.
/// It adds the provider as a transient service unless it is already added.
/// </param>
/// <param name="configureOptions">An optional delegate to configure the provider-specific options.</param>
/// <returns>The updated <see cref="OpenFeatureBuilder"/> instance with the new feature provider configured.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown if either <paramref name="builder"/> or <paramref name="domain"/> is null or if the <paramref name="domain"/> is empty.
/// </exception>
public static OpenFeatureBuilder AddProvider<TOptions>(this OpenFeatureBuilder builder, string domain, Func<IServiceProvider, string, FeatureProvider> implementationFactory, Action<TOptions>? configureOptions)
where TOptions : OpenFeatureOptions
{
Guard.ThrowIfNull(builder);

builder.DomainBoundProviderRegistrationCount++;

builder.Services.PostConfigure<TOptions>(options => options.AddProviderName(domain));
if (configureOptions != null)
{
builder.Services.Configure(domain, configureOptions);
}

builder.Services.TryAddKeyedTransient(domain, (provider, key) =>
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
return implementationFactory(provider, key.ToString()!);
});

builder.AddClient(domain);
return builder;
}

/// <summary>
/// Adds a feature provider for a specified domain using the default options.
/// This method configures a feature provider without custom options, delegating to the more generic AddProvider method.
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> used to configure feature flags.</param>
/// <param name="domain">The unique name of the provider.</param>
/// <param name="implementationFactory">
/// A factory method that creates a feature provider instance.
/// It adds the provider as a transient service unless it is already added.
/// </param>
/// <returns>The updated <see cref="OpenFeatureBuilder"/> instance with the new feature provider configured.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown if either <paramref name="builder"/> or <paramref name="domain"/> is null or if the <paramref name="domain"/> is empty.
/// </exception>
public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func<IServiceProvider, string, FeatureProvider> implementationFactory)
=> AddProvider<OpenFeatureOptions>(builder, domain, implementationFactory, configureOptions: null);

/// <summary>
/// Adds a feature client to the service collection, configuring it to work with a specific context if provided.
/// </summary>
Expand Down Expand Up @@ -230,35 +129,6 @@ private static IFeatureClient ResolveFeatureClient(IServiceProvider provider, st
return client;
}

/// <summary>
/// Configures policy name options for OpenFeature using the specified options type.
/// </summary>
/// <typeparam name="TOptions">The type of options used to configure <see cref="PolicyNameOptions"/>.</typeparam>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="configureOptions">A delegate to configure <typeparamref name="TOptions"/>.</param>
/// <returns>The configured <see cref="OpenFeatureBuilder"/> instance.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="builder"/> or <paramref name="configureOptions"/> is null.</exception>
public static OpenFeatureBuilder AddPolicyName<TOptions>(this OpenFeatureBuilder builder, Action<TOptions> configureOptions)
where TOptions : PolicyNameOptions
{
Guard.ThrowIfNull(builder);
Guard.ThrowIfNull(configureOptions);

builder.IsPolicyConfigured = true;

builder.Services.Configure(configureOptions);
return builder;
}

/// <summary>
/// Configures the default policy name options for OpenFeature.
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="configureOptions">A delegate to configure <see cref="PolicyNameOptions"/>.</param>
/// <returns>The configured <see cref="OpenFeatureBuilder"/> instance.</returns>
public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action<PolicyNameOptions> configureOptions)
=> AddPolicyName<PolicyNameOptions>(builder, configureOptions);

/// <summary>
/// Adds a feature hook to the service collection using a factory method. Hooks added here are not domain-bound.
/// </summary>
Expand Down
42 changes: 0 additions & 42 deletions src/OpenFeature.Hosting/OpenFeatureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,6 @@ namespace OpenFeature.Hosting;
/// </summary>
public class OpenFeatureOptions
{
private readonly HashSet<string> _providerNames = [];

/// <summary>
/// Determines if a default provider has been registered.
/// </summary>
public bool HasDefaultProvider { get; private set; }

/// <summary>
/// The type of the configured feature provider.
/// </summary>
public Type FeatureProviderType { get; protected internal set; } = null!;

/// <summary>
/// Gets a read-only list of registered provider names.
/// </summary>
public IReadOnlyCollection<string> ProviderNames => _providerNames;

/// <summary>
/// Registers the default provider name if no specific name is provided.
/// Sets <see cref="HasDefaultProvider"/> to true.
/// </summary>
protected internal void AddDefaultProviderName() => AddProviderName(null);

/// <summary>
/// Registers a new feature provider name. This operation is thread-safe.
/// </summary>
/// <param name="name">The name of the feature provider to register. Registers as default if null.</param>
protected internal void AddProviderName(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
HasDefaultProvider = true;
}
else
{
lock (_providerNames)
{
_providerNames.Add(name!);
}
}
}

private readonly HashSet<string> _hookNames = [];

internal IReadOnlyCollection<string> HookNames => _hookNames;
Expand Down
Loading
Loading