Skip to content
Open
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
1 change: 1 addition & 0 deletions OpenFeature.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<Project Path="test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj" />
<Project Path="test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj" />
<Project Path="test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj" />
<Project Path="test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj" />
<Project Path="test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj" />
<Project Path="test/OpenFeature.Tests/OpenFeature.Tests.csproj" />
</Folder>
Expand Down
4 changes: 2 additions & 2 deletions samples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using OpenFeature;
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
using OpenFeature.Hosting.Providers.Memory;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;
using OpenTelemetry.Metrics;
Expand Down Expand Up @@ -38,7 +38,7 @@
.WithFlagEvaluationMetadata("boolean", s => s.GetBool("boolean"))
.Build();

featureBuilder.AddHostedFeatureLifecycle()
featureBuilder
.AddHook(sp => new LoggingHook(sp.GetRequiredService<ILogger<LoggingHook>>()))
.AddHook(_ => new MetricsHook(metricsHookOptions))
.AddHook<TraceEnricherHook>()
Expand Down
1 change: 0 additions & 1 deletion samples/AspNetCore/Samples.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj" />
<ProjectReference Include="..\..\src\OpenFeature.Hosting\OpenFeature.Hosting.csproj" />
<ProjectReference Include="..\..\src\OpenFeature\OpenFeature.csproj" />
</ItemGroup>
Expand Down
38 changes: 38 additions & 0 deletions src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace OpenFeature.Hosting.Diagnostics;

/// <summary>
/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework.
/// </summary>
/// <remarks>
/// <c>Experimental</c> - This class includes identifiers that allow developers to track and conditionally enable
/// experimental features. Each identifier follows a structured code format to indicate the feature domain,
/// maturity level, and unique identifier. Note that experimental features are subject to change or removal
/// in future releases.
/// <para>
/// <strong>Basic Information</strong><br/>
/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize
/// and manage experimental features effectively.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// Code Structure:
/// - "OF" - Represents the OpenFeature library.
/// - "DI" - Indicates the Dependency Injection domain.
/// - "001" - Unique identifier for a specific feature.
/// </code>
/// </example>
internal static class FeatureCodes
{
/// <summary>
/// Identifier for the experimental Dependency Injection features within the OpenFeature framework.
/// </summary>
/// <remarks>
/// <c>OFDI001</c> identifier marks experimental features in the Dependency Injection (DI) domain.
///
/// Usage:
/// Developers can use this identifier to conditionally enable or test experimental DI features.
/// It is part of the OpenFeature diagnostics system to help track experimental functionality.
/// </remarks>
public const string NewDi = "OFDI001";
}
14 changes: 14 additions & 0 deletions src/OpenFeature.Hosting/Guard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace OpenFeature.Hosting;

[DebuggerStepThrough]
internal static class Guard
{
public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (argument is null)
throw new ArgumentNullException(paramName);
}
}
1 change: 0 additions & 1 deletion src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenFeature.DependencyInjection;

namespace OpenFeature.Hosting;

Expand Down
24 changes: 24 additions & 0 deletions src/OpenFeature.Hosting/IFeatureLifecycleManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace OpenFeature.Hosting;

/// <summary>
/// Defines the contract for managing the lifecycle of a feature api.
/// </summary>
public interface IFeatureLifecycleManager
{
/// <summary>
/// Ensures that the feature provider is properly initialized and ready to be used.
/// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider.
/// </summary>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns>A Task representing the asynchronous operation of initializing the feature provider.</returns>
/// <exception cref="InvalidOperationException">Thrown when the feature provider is not registered or is in an invalid state.</exception>
ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved.
/// This method should handle all necessary cleanup and shutdown operations for the feature provider.
/// </summary>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns>A Task representing the asynchronous operation of shutting down the feature provider.</returns>
ValueTask ShutdownAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using OpenFeature.Constant;
using OpenFeature.Model;

namespace OpenFeature.Hosting.Internal;

internal record EventHandlerDelegateWrapper(
ProviderEventTypes ProviderEventType,
EventHandlerDelegate EventHandlerDelegate);
66 changes: 66 additions & 0 deletions src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace OpenFeature.Hosting.Internal;

internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager
{
private readonly Api _featureApi;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<FeatureLifecycleManager> _logger;

public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger<FeatureLifecycleManager> logger)
{
_featureApi = featureApi;
_serviceProvider = serviceProvider;
_logger = logger;
}

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

var options = _serviceProvider.GetRequiredService<IOptions<OpenFeatureOptions>>().Value;
if (options.HasDefaultProvider)
{
var featureProvider = _serviceProvider.GetRequiredService<FeatureProvider>();
await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false);
}

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

var hooks = new List<Hook>();
foreach (var hookName in options.HookNames)
{
var hook = _serviceProvider.GetRequiredKeyedService<Hook>(hookName);
hooks.Add(hook);
}

_featureApi.AddHooks(hooks);

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();
await _featureApi.ShutdownAsync().ConfigureAwait(false);
}

[LoggerMessage(200, LogLevel.Information, "Starting initialization of the feature provider")]
partial void LogStartingInitializationOfFeatureProvider();

[LoggerMessage(200, LogLevel.Information, "Shutting down the feature provider")]
partial void LogShuttingDownFeatureProvider();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @formatter:off
// ReSharper disable All
#if NETCOREAPP3_0_OR_GREATER
// https://github.com/dotnet/runtime/issues/96197
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))]
#else
#pragma warning disable
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}

public string ParameterName { get; }
}
#endif
21 changes: 21 additions & 0 deletions src/OpenFeature.Hosting/MultiTarget/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @formatter:off
// ReSharper disable All
#if NET5_0_OR_GREATER
// https://github.com/dotnet/runtime/issues/96197
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
#else
#pragma warning disable
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;

namespace System.Runtime.CompilerServices;

/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
static class IsExternalInit { }
#endif
11 changes: 8 additions & 3 deletions src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net462</TargetFrameworks>
<RootNamespace>OpenFeature</RootNamespace>
</PropertyGroup>

Expand All @@ -10,7 +10,12 @@
</ItemGroup>

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

<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>

</Project>
60 changes: 60 additions & 0 deletions src/OpenFeature.Hosting/OpenFeatureBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.Extensions.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)
{
/// <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
/// should be initialized based on the presence of an evaluation context.
/// </summary>
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.
/// </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.");
}
}
}
}
Loading
Loading