Skip to content

Commit 225b58d

Browse files
committed
Add Dependency Injection code to Hosting package
Signed-off-by: Kyle Julian <[email protected]>
1 parent 6e521d2 commit 225b58d

15 files changed

+905
-3
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace OpenFeature.DependencyInjection.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+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Diagnostics;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace OpenFeature.DependencyInjection;
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+
15+
public static void ThrowIfNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
16+
{
17+
if (string.IsNullOrWhiteSpace(argument))
18+
throw new ArgumentNullException(paramName);
19+
}
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace OpenFeature.DependencyInjection;
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.DependencyInjection.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.DependencyInjection.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
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// @formatter:off
2+
// ReSharper disable All
3+
#if NET5_0_OR_GREATER
4+
// https://github.com/dotnet/runtime/issues/96197
5+
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
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+
using System.ComponentModel;
12+
13+
namespace System.Runtime.CompilerServices;
14+
15+
/// <summary>
16+
/// Reserved to be used by the compiler for tracking metadata.
17+
/// This class should not be used by developers in source code.
18+
/// </summary>
19+
[EditorBrowsable(EditorBrowsableState.Never)]
20+
static class IsExternalInit { }
21+
#endif
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace OpenFeature.DependencyInjection;
4+
5+
/// <summary>
6+
/// Describes a <see cref="OpenFeatureBuilder"/> backed by an <see cref="IServiceCollection"/>.
7+
/// </summary>
8+
/// <param name="services">The services being configured.</param>
9+
public class OpenFeatureBuilder(IServiceCollection services)
10+
{
11+
/// <summary> The services being configured. </summary>
12+
public IServiceCollection Services { get; } = services;
13+
14+
/// <summary>
15+
/// Indicates whether the evaluation context has been configured.
16+
/// This property is used to determine if specific configurations or services
17+
/// should be initialized based on the presence of an evaluation context.
18+
/// </summary>
19+
public bool IsContextConfigured { get; internal set; }
20+
21+
/// <summary>
22+
/// Indicates whether the policy has been configured.
23+
/// </summary>
24+
public bool IsPolicyConfigured { get; internal set; }
25+
26+
/// <summary>
27+
/// Gets a value indicating whether a default provider has been registered.
28+
/// </summary>
29+
public bool HasDefaultProvider { get; internal set; }
30+
31+
/// <summary>
32+
/// Gets the count of domain-bound providers that have been registered.
33+
/// This count does not include the default provider.
34+
/// </summary>
35+
public int DomainBoundProviderRegistrationCount { get; internal set; }
36+
37+
/// <summary>
38+
/// Validates the current configuration, ensuring that a policy is set when multiple providers are registered
39+
/// or when a default provider is registered alongside another provider.
40+
/// </summary>
41+
/// <exception cref="InvalidOperationException">
42+
/// Thrown if multiple providers are registered without a policy, or if both a default provider
43+
/// and an additional provider are registered without a policy configuration.
44+
/// </exception>
45+
public void Validate()
46+
{
47+
if (!IsPolicyConfigured)
48+
{
49+
if (DomainBoundProviderRegistrationCount > 1)
50+
{
51+
throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured.");
52+
}
53+
54+
if (HasDefaultProvider && DomainBoundProviderRegistrationCount == 1)
55+
{
56+
throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration.");
57+
}
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)