Skip to content

Commit fdf2297

Browse files
feat: Deprecate AddHostedFeatureLifecycle method (#531)
* Add Dependency Injection code to Hosting package Signed-off-by: Kyle Julian <[email protected]> * Add HostedFeatureLifecycleService when AddOpenFeature is called * Update Samples App to show how you can work with OpenFeature and Dependency Injection Signed-off-by: Kyle Julian <[email protected]> * Migrate Dependency Injection tests over to the Hosting code * Copy the existing Dependency Injection tests over to a new Hosting.Tests project * Fix issue with the Integration tests Signed-off-by: Kyle Julian <[email protected]> * Fix issue post rebase Signed-off-by: Kyle Julian <[email protected]> * Move DI classes into Hosting namespace Signed-off-by: Kyle Julian <[email protected]> * Address linting issue Signed-off-by: Kyle Julian <[email protected]> * Fix issue with unit tests failing on Ubuntu Signed-off-by: Kyle Julian <[email protected]> * Add additional unit tests to improve test coverage Signed-off-by: Kyle Julian <[email protected]> * Add more unit tests to improve unit test coverage Signed-off-by: Kyle Julian <[email protected]> * Fix formating issues and flaky test Signed-off-by: Kyle Julian <[email protected]> * Improve unit test coverage and remove duplicate test file Signed-off-by: Kyle Julian <[email protected]> * Apply dotnet format fixes Signed-off-by: Kyle Julian <[email protected]> --------- Signed-off-by: Kyle Julian <[email protected]>
1 parent 76bd94b commit fdf2297

32 files changed

+2357
-13
lines changed

OpenFeature.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Project Path="test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj" />
6363
<Project Path="test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj" />
6464
<Project Path="test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj" />
65+
<Project Path="test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj" />
6566
<Project Path="test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj" />
6667
<Project Path="test/OpenFeature.Tests/OpenFeature.Tests.csproj" />
6768
<Project Path="test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj" />

samples/AspNetCore/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
using System.Text.Json.Serialization;
33
using Microsoft.AspNetCore.Mvc;
44
using OpenFeature;
5-
using OpenFeature.DependencyInjection.Providers.Memory;
65
using OpenFeature.Hooks;
6+
using OpenFeature.Hosting.Providers.Memory;
77
using OpenFeature.Model;
88
using OpenFeature.Providers.Memory;
99
using OpenFeature.Providers.MultiProvider;
@@ -41,7 +41,7 @@
4141
.WithFlagEvaluationMetadata("boolean", s => s.GetBool("boolean"))
4242
.Build();
4343

44-
featureBuilder.AddHostedFeatureLifecycle()
44+
featureBuilder
4545
.AddHook(sp => new LoggingHook(sp.GetRequiredService<ILogger<LoggingHook>>()))
4646
.AddHook(_ => new MetricsHook(metricsHookOptions))
4747
.AddHook<TraceEnricherHook>()

samples/AspNetCore/Samples.AspNetCore.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<ProjectReference Include="..\..\src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj" />
1110
<ProjectReference Include="..\..\src\OpenFeature.Hosting\OpenFeature.Hosting.csproj" />
1211
<ProjectReference Include="..\..\src\OpenFeature.Providers.MultiProvider\OpenFeature.Providers.MultiProvider.csproj" />
1312
<ProjectReference Include="..\..\src\OpenFeature\OpenFeature.csproj" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace OpenFeature.Hosting.Diagnostics;
2+
3+
/// <summary>
4+
/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework.
5+
/// </summary>
6+
/// <remarks>
7+
/// <c>Experimental</c> - This class includes identifiers that allow developers to track and conditionally enable
8+
/// experimental features. Each identifier follows a structured code format to indicate the feature domain,
9+
/// maturity level, and unique identifier. Note that experimental features are subject to change or removal
10+
/// in future releases.
11+
/// <para>
12+
/// <strong>Basic Information</strong><br/>
13+
/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize
14+
/// and manage experimental features effectively.
15+
/// </para>
16+
/// </remarks>
17+
/// <example>
18+
/// <code>
19+
/// Code Structure:
20+
/// - "OF" - Represents the OpenFeature library.
21+
/// - "DI" - Indicates the Dependency Injection domain.
22+
/// - "001" - Unique identifier for a specific feature.
23+
/// </code>
24+
/// </example>
25+
internal static class FeatureCodes
26+
{
27+
/// <summary>
28+
/// Identifier for the experimental Dependency Injection features within the OpenFeature framework.
29+
/// </summary>
30+
/// <remarks>
31+
/// <c>OFDI001</c> identifier marks experimental features in the Dependency Injection (DI) domain.
32+
///
33+
/// Usage:
34+
/// Developers can use this identifier to conditionally enable or test experimental DI features.
35+
/// It is part of the OpenFeature diagnostics system to help track experimental functionality.
36+
/// </remarks>
37+
public const string NewDi = "OFDI001";
38+
}

src/OpenFeature.Hosting/Guard.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Diagnostics;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace OpenFeature.Hosting;
5+
6+
[DebuggerStepThrough]
7+
internal static class Guard
8+
{
9+
public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
10+
{
11+
if (argument is null)
12+
throw new ArgumentNullException(paramName);
13+
}
14+
}

src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using Microsoft.Extensions.Hosting;
22
using Microsoft.Extensions.Logging;
33
using Microsoft.Extensions.Options;
4-
using OpenFeature.DependencyInjection;
54

65
namespace OpenFeature.Hosting;
76

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace OpenFeature.Hosting;
2+
3+
/// <summary>
4+
/// Defines the contract for managing the lifecycle of a feature api.
5+
/// </summary>
6+
public interface IFeatureLifecycleManager
7+
{
8+
/// <summary>
9+
/// Ensures that the feature provider is properly initialized and ready to be used.
10+
/// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider.
11+
/// </summary>
12+
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
13+
/// <returns>A Task representing the asynchronous operation of initializing the feature provider.</returns>
14+
/// <exception cref="InvalidOperationException">Thrown when the feature provider is not registered or is in an invalid state.</exception>
15+
ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default);
16+
17+
/// <summary>
18+
/// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved.
19+
/// This method should handle all necessary cleanup and shutdown operations for the feature provider.
20+
/// </summary>
21+
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
22+
/// <returns>A Task representing the asynchronous operation of shutting down the feature provider.</returns>
23+
ValueTask ShutdownAsync(CancellationToken cancellationToken = default);
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using OpenFeature.Constant;
2+
using OpenFeature.Model;
3+
4+
namespace OpenFeature.Hosting.Internal;
5+
6+
internal record EventHandlerDelegateWrapper(
7+
ProviderEventTypes ProviderEventType,
8+
EventHandlerDelegate EventHandlerDelegate);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
5+
namespace OpenFeature.Hosting.Internal;
6+
7+
internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager
8+
{
9+
private readonly Api _featureApi;
10+
private readonly IServiceProvider _serviceProvider;
11+
private readonly ILogger<FeatureLifecycleManager> _logger;
12+
13+
public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger<FeatureLifecycleManager> logger)
14+
{
15+
_featureApi = featureApi;
16+
_serviceProvider = serviceProvider;
17+
_logger = logger;
18+
}
19+
20+
/// <inheritdoc />
21+
public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default)
22+
{
23+
this.LogStartingInitializationOfFeatureProvider();
24+
25+
var options = _serviceProvider.GetRequiredService<IOptions<OpenFeatureOptions>>().Value;
26+
if (options.HasDefaultProvider)
27+
{
28+
var featureProvider = _serviceProvider.GetRequiredService<FeatureProvider>();
29+
await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false);
30+
}
31+
32+
foreach (var name in options.ProviderNames)
33+
{
34+
var featureProvider = _serviceProvider.GetRequiredKeyedService<FeatureProvider>(name);
35+
await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false);
36+
}
37+
38+
var hooks = new List<Hook>();
39+
foreach (var hookName in options.HookNames)
40+
{
41+
var hook = _serviceProvider.GetRequiredKeyedService<Hook>(hookName);
42+
hooks.Add(hook);
43+
}
44+
45+
_featureApi.AddHooks(hooks);
46+
47+
var handlers = _serviceProvider.GetServices<EventHandlerDelegateWrapper>();
48+
foreach (var handler in handlers)
49+
{
50+
_featureApi.AddHandler(handler.ProviderEventType, handler.EventHandlerDelegate);
51+
}
52+
}
53+
54+
/// <inheritdoc />
55+
public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default)
56+
{
57+
this.LogShuttingDownFeatureProvider();
58+
await _featureApi.ShutdownAsync().ConfigureAwait(false);
59+
}
60+
61+
[LoggerMessage(200, LogLevel.Information, "Starting initialization of the feature provider")]
62+
partial void LogStartingInitializationOfFeatureProvider();
63+
64+
[LoggerMessage(200, LogLevel.Information, "Shutting down the feature provider")]
65+
partial void LogShuttingDownFeatureProvider();
66+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// @formatter:off
2+
// ReSharper disable All
3+
#if NETCOREAPP3_0_OR_GREATER
4+
// https://github.com/dotnet/runtime/issues/96197
5+
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))]
6+
#else
7+
#pragma warning disable
8+
// Licensed to the .NET Foundation under one or more agreements.
9+
// The .NET Foundation licenses this file to you under the MIT license.
10+
11+
namespace System.Runtime.CompilerServices;
12+
13+
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
14+
internal sealed class CallerArgumentExpressionAttribute : Attribute
15+
{
16+
public CallerArgumentExpressionAttribute(string parameterName)
17+
{
18+
ParameterName = parameterName;
19+
}
20+
21+
public string ParameterName { get; }
22+
}
23+
#endif

0 commit comments

Comments
 (0)