Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Dependency Injection and Hosting support for OpenFeature #310

Open
wants to merge 97 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
963b54c
[SIP-123] feat: Create OpenFeature.DependencyInjection project
arttonoyan Oct 4, 2024
a803429
chore: Update TargetFrameworks to net6.0
arttonoyan Oct 4, 2024
79ee277
feat: Create OpenFeatureBuilder record
arttonoyan Oct 4, 2024
9f3c40e
feat: Add OpenFeatureBuilderExtensions
arttonoyan Oct 4, 2024
6786bd8
feat: Add IFeatureLifecycleManager interface and implementation
arttonoyan Oct 4, 2024
f373eb0
feat: Add OpenFeatureServiceCollectionExtensions
arttonoyan Oct 4, 2024
1f86298
feat: Add AddProvider extension method in OpenFeatureBuilderExtensions
arttonoyan Oct 8, 2024
f1039a2
feat: Add AddProvider extension method in OpenFeatureServiceCollectio…
arttonoyan Oct 13, 2024
5905cc6
test: Add OpenFeatureBuilderExtensionsTests
arttonoyan Oct 13, 2024
6c3c15e
test: Add unit tests for AddProvider method in OpenFeatureBuilderExte…
arttonoyan Oct 13, 2024
bd20d40
feat: Replicate NoOpFeatureProvider implementation from src/OpenFeatu…
arttonoyan Oct 13, 2024
441aa4f
test: Update OpenFeatureBuilderExtensionsTests with NoOpFeatureProvid…
arttonoyan Oct 13, 2024
fa04665
test: Add OpenFeatureBuilderExtensionsTests
arttonoyan Oct 13, 2024
fcea8a6
refactor: Remove ExcludeFromCodeCoverage attribute from FeatureLifecy…
arttonoyan Oct 13, 2024
fc8dcab
test: Improve OpenFeatureBuilderExtensionsTests for better coverage a…
arttonoyan Oct 13, 2024
0a996aa
test: Create OpenFeatureServiceCollectionExtensionsTests
arttonoyan Oct 13, 2024
76e1816
feat: Create OpenFeature.Hosting project
arttonoyan Oct 13, 2024
84dd0e0
feat: Add HostedFeatureLifecycleService and related logic
arttonoyan Oct 13, 2024
572d67b
test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProvide…
arttonoyan Oct 14, 2024
d7e5fbb
[SIP-123] feat: Create OpenFeature.DependencyInjection project
arttonoyan Oct 4, 2024
f60b5d8
chore: Update TargetFrameworks to net6.0
arttonoyan Oct 4, 2024
8b59e53
feat: Create OpenFeatureBuilder record
arttonoyan Oct 4, 2024
c644df6
feat: Add OpenFeatureBuilderExtensions
arttonoyan Oct 4, 2024
f4f108b
feat: Add IFeatureLifecycleManager interface and implementation
arttonoyan Oct 4, 2024
43bcde9
feat: Add OpenFeatureServiceCollectionExtensions
arttonoyan Oct 4, 2024
47e87c2
feat: Add AddProvider extension method in OpenFeatureBuilderExtensions
arttonoyan Oct 8, 2024
3650de2
feat: Add AddProvider extension method in OpenFeatureServiceCollectio…
arttonoyan Oct 13, 2024
0027489
test: Add OpenFeatureBuilderExtensionsTests
arttonoyan Oct 13, 2024
5638163
test: Add unit tests for AddProvider method in OpenFeatureBuilderExte…
arttonoyan Oct 13, 2024
5de7ceb
feat: Replicate NoOpFeatureProvider implementation from src/OpenFeatu…
arttonoyan Oct 13, 2024
2e99210
test: Update OpenFeatureBuilderExtensionsTests with NoOpFeatureProvid…
arttonoyan Oct 13, 2024
880df92
test: Add OpenFeatureBuilderExtensionsTests
arttonoyan Oct 13, 2024
9258706
refactor: Remove ExcludeFromCodeCoverage attribute from FeatureLifecy…
arttonoyan Oct 13, 2024
19664b0
test: Improve OpenFeatureBuilderExtensionsTests for better coverage a…
arttonoyan Oct 13, 2024
9c21943
test: Create OpenFeatureServiceCollectionExtensionsTests
arttonoyan Oct 13, 2024
a23c5ea
feat: Create OpenFeature.Hosting project
arttonoyan Oct 13, 2024
7906a3b
feat: Add HostedFeatureLifecycleService and related logic
arttonoyan Oct 13, 2024
1850159
test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProvide…
arttonoyan Oct 14, 2024
63ead66
Merge branch 'add-dependency-injection' of https://github.com/arttono…
arttonoyan Oct 14, 2024
70708ec
fix(build): Resolve MultiTarget framework build errors
arttonoyan Oct 14, 2024
d615b04
chore(deps): update codecov/codecov-action action to v4.5.0 (#272)
renovate[bot] Jul 3, 2024
ad95765
chore(deps): update dependency githubactionstestlogger to v2.4.1 (#274)
renovate[bot] Jul 3, 2024
ce23cdc
feat!: internally maintain provider status (#276)
toddbaert Jul 3, 2024
7716f4a
chore: cleanup code (#277)
askpt Jul 23, 2024
12d48c2
chore(deps): update actions/upload-artifact action to v4.3.4 (#278)
renovate[bot] Jul 25, 2024
1f0468e
chore(deps): update xunit-dotnet monorepo (#279)
renovate[bot] Jul 25, 2024
d480da4
chore(deps): update dependency dotnet-sdk to v8.0.303 (#275)
renovate[bot] Jul 25, 2024
cc5f5bc
feat: Drop net7 TFM (#284)
benjiro Jul 26, 2024
90d172c
fix: Should map metadata when converting from ResolutionDetails to Fl…
benjiro Jul 26, 2024
2072e45
feat: back targetingKey with internal map (#287)
toddbaert Jul 29, 2024
c503b30
chore(deps): update actions/upload-artifact action to v4.3.5 (#291)
renovate[bot] Aug 6, 2024
9a5a343
feat!: domain instead of client name (#294)
toddbaert Aug 13, 2024
be43fcc
chore(deps): update dependency benchmarkdotnet to v0.14.0 (#293)
renovate[bot] Aug 14, 2024
84f8e18
chore(deps): update dependency dotnet-sdk to v8.0.400 (#295)
renovate[bot] Aug 14, 2024
94f4d66
chore: in-memory UpdateFlags to UpdateFlagsAsync (#298)
toddbaert Aug 21, 2024
756a7f7
chore(main): release 2.0.0 (#254)
github-actions[bot] Aug 21, 2024
36b5f8c
chore(deps): update dependency microsoft.net.test.sdk to 17.11.0 (#297)
renovate[bot] Sep 4, 2024
c9339ec
chore(deps): update dependency dotnet-sdk to v8.0.401 (#296)
renovate[bot] Sep 5, 2024
980d0be
chore: update release please config (#304)
beeme1mr Sep 24, 2024
d4a5e3d
[SIP-123] feat: Create OpenFeature.DependencyInjection project
arttonoyan Oct 4, 2024
8387def
chore: Update TargetFrameworks to net6.0
arttonoyan Oct 4, 2024
5077a1c
feat: Create OpenFeatureBuilder record
arttonoyan Oct 4, 2024
95e8ddc
feat: Add OpenFeatureBuilderExtensions
arttonoyan Oct 4, 2024
f157496
feat: Add IFeatureLifecycleManager interface and implementation
arttonoyan Oct 4, 2024
49991ec
feat: Add OpenFeatureServiceCollectionExtensions
arttonoyan Oct 4, 2024
5bc2c47
feat: Add AddProvider extension method in OpenFeatureBuilderExtensions
arttonoyan Oct 8, 2024
9ea9b01
feat: Add AddProvider extension method in OpenFeatureServiceCollectio…
arttonoyan Oct 13, 2024
4066b89
test: Add OpenFeatureBuilderExtensionsTests
arttonoyan Oct 13, 2024
367518b
test: Add unit tests for AddProvider method in OpenFeatureBuilderExte…
arttonoyan Oct 13, 2024
70c269f
feat: Replicate NoOpFeatureProvider implementation from src/OpenFeatu…
arttonoyan Oct 13, 2024
b20bc13
test: Update OpenFeatureBuilderExtensionsTests with NoOpFeatureProvid…
arttonoyan Oct 13, 2024
c251f56
test: Add OpenFeatureBuilderExtensionsTests
arttonoyan Oct 13, 2024
8b2ab4b
refactor: Remove ExcludeFromCodeCoverage attribute from FeatureLifecy…
arttonoyan Oct 13, 2024
3800447
test: Improve OpenFeatureBuilderExtensionsTests for better coverage a…
arttonoyan Oct 13, 2024
e111714
test: Create OpenFeatureServiceCollectionExtensionsTests
arttonoyan Oct 13, 2024
cb63b1c
feat: Create OpenFeature.Hosting project
arttonoyan Oct 13, 2024
0e53494
feat: Add HostedFeatureLifecycleService and related logic
arttonoyan Oct 13, 2024
dc765e6
test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProvide…
arttonoyan Oct 14, 2024
4f80593
add-dependency-injection
arttonoyan Oct 16, 2024
0a76193
add-dependency-injection
arttonoyan Oct 16, 2024
ac98ddd
add-dependency-injection
arttonoyan Oct 16, 2024
4af39ce
add-dependency-injection
arttonoyan Oct 16, 2024
0fa6dc3
add-dependency-injection
arttonoyan Oct 16, 2024
926c0d4
add-dependency-injection
arttonoyan Oct 16, 2024
9b8d9f2
add-dependency-injection
arttonoyan Oct 16, 2024
aa488ae
feat: Replicate NoOpFeatureProvider implementation from src/OpenFeatu…
arttonoyan Oct 13, 2024
1f8e9e4
add-dependency-injection
arttonoyan Oct 16, 2024
3aaa7df
test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProvide…
arttonoyan Oct 14, 2024
ffef1ac
add-dependency-injection
arttonoyan Oct 16, 2024
1acb4fa
add-dependency-injection
arttonoyan Oct 16, 2024
96889dd
Merge branch 'main' into add-dependency-injection
arttonoyan Oct 16, 2024
54c145e
Fix dotnet format build.
askpt Oct 17, 2024
55582d7
Changing to Source Generation logging.
askpt Oct 18, 2024
8ebdad4
add-dependency-injection
arttonoyan Oct 21, 2024
3089129
Merge branch 'add-dependency-injection' of https://github.com/arttono…
arttonoyan Oct 21, 2024
fe5e072
refactor: Update ThrowIfNull method
arttonoyan Oct 21, 2024
10ea9f1
refactor: Change OpenFeatureBuilder to class and make IsContextConfig…
arttonoyan Oct 22, 2024
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
13 changes: 8 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<Project>

<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup Label="src">
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="1.7.1" />
<PackageVersion Include="System.Threading.Channels" Version="6.0.0" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>

<ItemGroup Label="test">
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
Expand All @@ -26,10 +28,11 @@
<PackageVersion Include="SpecFlow.xUnit" Version="3.9.74" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(OS)' == 'Unix'">
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
</ItemGroup>

</Project>
35 changes: 28 additions & 7 deletions OpenFeature.sln
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -101,21 +107,36 @@ Global
{7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU
{C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU
{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
{09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
{4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
{09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
{72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94}
{C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F}
Expand Down
24 changes: 24 additions & 0 deletions src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace OpenFeature;

/// <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,37 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace OpenFeature.Internal;

internal sealed 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)
{
_logger.LogInformation("Starting initialization of the feature provider");
askpt marked this conversation as resolved.
Show resolved Hide resolved
var featureProvider = _serviceProvider.GetService<FeatureProvider>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason for not resolving FeatureProvider directly in the ctor beside having custom exception for that situation? In effect it not allow call-site validation to detect that FeatureProvider is missing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason for not resolving FeatureProvider directly in the constructor is that some implementations of FeatureProvider are not fully controlled by us, and they may execute significant logic during construction. For example, certain providers like LaunchDarkly perform extensive initialization within the constructor, which can take time and potentially fail (as seen in LaunchDarkly's Provider Implementation and LdClient Constructor).

By not resolving FeatureProvider in the constructor, we can safely perform this logic asynchronously in a start method, which avoids delays or failures during object instantiation. Additionally, I use GetService to log meaningful messages in case of any issues.

In short, this approach allows us to handle potential failures asynchronously during startup and ensures proper logging in case of errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally providers are doing such initialization in their initialize() method, but we can't control them all, and I see your point. Generally I'd prefer constructor resolution as well but I can understand the advantages you describe.

if (featureProvider == null)
{
throw new InvalidOperationException("Feature provider is not registered in the service collection.");
}
await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false);
Copy link
Member

@toddbaert toddbaert Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want an optional value for domain, or some other method for working with domain-scoped providers.

}

/// <inheritdoc />
public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default)
{
_logger.LogInformation("Shutting down the feature provider.");
askpt marked this conversation as resolved.
Show resolved Hide resolved
await _featureApi.ShutdownAsync().ConfigureAwait(false);
}
}
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider using PolySharp. It is smart and customizable source generator which will detect what exact feature need to be poly-filled and generate the code for that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I’ll definitely take a closer look and see how it can fit into our current approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arttonoyan / @wwalendz-relativity, thanks for bringing this to our attention. I need someone else to confirm if bringing an extra library to use these polyfils is okay.

@toddbaert or @beeme1mr ?

{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}

public string ParameterName { get; }
}
#endif
20 changes: 20 additions & 0 deletions src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace OpenFeature;

[DebuggerStepThrough]
internal static class Guard
{
public static T ThrowIfNull<T>(T? value, [CallerArgumentExpression("value")] string name = null!)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(value, name);
#else
if (value is null)
throw new ArgumentNullException(name);
#endif

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think it's better just do use the version in the #else here; using both a "nice" API and a "less nice" API is more complex than just using the "less nice" API, IMO.

Unless there some performance implication here I think this just hurts readability a bit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

return value;
}
}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0;net462</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>OpenFeature</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

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

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<Folder Include="MultiTarget\" />
</ItemGroup>

</Project>
17 changes: 17 additions & 0 deletions src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;

namespace OpenFeature;

/// <summary>
/// Describes a <see cref="OpenFeatureBuilder"/> backed by an <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="Services">The <see cref="IServiceCollection"/> instance.</param>
public sealed record OpenFeatureBuilder(IServiceCollection Services)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the pattern/reasoning behind using a sealed record and extensions here?
I have not too much experience with .NET so I am mostly wondering why this is the chosen/preferred way.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukas-reining Thanks for your review.

Reasoning for using a record in OpenFeatureBuilder and extension methods:

  1. Immutability:
    Using a record in OpenFeatureBuilder ensures immutability, which is crucial in configurations where state changes should not happen after initialization. Records in C# provide an easy way to define immutable types and handle value-based equality. This matches the pattern used in other parts of ASP.NET Core, like the AuthenticationBuilder class, where the object holds configuration data (like IServiceCollection) and doesn’t change its internal state directly.

  2. Following .NET Patterns (e.g., AuthenticationBuilder):
    The AuthenticationBuilder is a great example from ASP.NET Core, where the builder encapsulates IServiceCollection and allows adding services via extension methods. Our approach to OpenFeatureBuilder follows the same design, where extension methods add specific configuration options, making the builder pattern more extensible without altering its core structure. Microsoft actively uses this builder pattern across many of their packages, not just for authentication. It's proven to be a robust way to add functionality in a clean and modular manner.

  3. Extension Methods Simplify Extensibility:
    Like how GoogleExtensions.cs and other authentication schemes are added via extension methods on AuthenticationBuilder, the same idea applies to OpenFeatureBuilder. This allows developers to extend the functionality of OpenFeatureBuilder in a modular way, adding new features without needing to modify or subclass the builder itself. This aligns with Microsoft’s general approach to keep core objects clean and extensible via extensions.

  4. Many Uses Across Microsoft Packages:
    While AuthenticationBuilder is a useful example, this builder and extension pattern is widely used throughout Microsoft’s packages, such as in DI (Dependency Injection), logging, and configuration. It allows developers to write clean, easily maintainable code that is open to extensions without modifying the core framework.

  5. Reconsidering the sealed Keyword:
    In some cases, the use of sealed on a builder might restrict extensibility via inheritance. Although the builder pattern usually focuses on extension methods rather than inheritance, removing sealed can be beneficial if you envision a need for developers to extend OpenFeatureBuilder through inheritance. It opens up the possibility for advanced users to create custom builders while still maintaining the base functionality.


In summary, the use of a record for OpenFeatureBuilder fits well with established patterns in .NET, like AuthenticationBuilder, making it immutable and easily extensible through extension methods. The use of this pattern is not isolated; it’s widespread across Microsoft packages and proves to be a useful approach. The consideration of removing sealed can further enhance the extensibility of the builder, allowing more advanced use cases where inheritance might be needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove the sealed modifier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @arttonoyan!
I am still not sure why we use the record.

Using a record in OpenFeatureBuilder ensures immutability, which is crucial in configurations where state changes should not happen after initialization.

If I am not looking at the wrong place, we do not have any data.
The AuthenticationBuilder is a class and not a record.

The AuthenticationBuilder also provides the basic functionality directly so maybe we would only need the extensions from src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs but we could inline src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs as they are directly defined in the same package.

I have no hard opinion on this, I would just like to understand the reasoning.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in our case, there isn't a significant difference between using a class or a record. The main reason I opted for a record is because it allows for a more concise and compact syntax, especially when the class primarily holds immutable data, like the Services property. Functionally, it would be nearly identical to using a class like this:

public class OpenFeatureBuilder
{
    public OpenFeatureBuilder(IServiceCollection services)
    {
        Services = services;
    }

    public IServiceCollection Services { get; }
}

Copy link
Member

@lukas-reining lukas-reining Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

especially when the class primarily holds immutable data, like the Services property.

In our case we have one immutable property Services and one mutable IsContextConfigured.
This is not too important I guess but I am just wondering why we should do different then e.g. ASP.NET Core in your example.

As you said you removed sealed for extensibility, but I would argue that classes, as for AuthenticationBuilder, would be more idiomatic and extensible than extending a record as "a class can not inherit from a record" and the pattern for extending OpenFeature would be different than for the ASP.NET Core built-ins: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#inheritance

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me the use of record also caused confusion. I had to read a bit to understand why we'd use it in this case.

Personally, since other builders we have are implemented as classes I think I'd prefer that, but it's not a strong preference.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukas-reining @toddbaert Thank you for your thoughtful feedback! I completely understand your point, especially regarding consistency with other builders being implemented as classes. I can see how using a record in this case might cause some confusion. It’s not a strong preference for me either, I will change with the more familiar pattern of using classes for builders.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add, other reason why we should seal classes is in dotnet 6, we got some small performance improvements: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#“peanut-butter”

As for the record, I would prefer to start using it by the reasons that @arttonoyan mentioned before. But I am happy to keep as a class until we decide to move it to record.

{
/// <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>
internal bool IsContextConfigured { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure: why is this internal?
If our package benefits from IsContextConfigured could it be beneficial for external extensions too?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question! I initially made it internal because it was primarily used for internal configuration, and the public setter was intended for use within the internal scope. However, you raise a good point. There's no strong reason not to make it public, as it could provide useful information for consumers. We can certainly change it to:

public bool IsContextConfigured { get; internal set; }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenFeature.Model;

namespace OpenFeature;

/// <summary>
/// Contains extension methods for the <see cref="OpenFeatureBuilder"/> class.
/// </summary>
public static partial class OpenFeatureBuilderExtensions
{
/// <summary>
/// This method is used to add a new context to the service collection.
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="configure">the desired configuration</param>
/// <returns>The <see cref="OpenFeatureBuilder"/> instance.</returns>
public static OpenFeatureBuilder AddContext(
this OpenFeatureBuilder builder,
Action<EvaluationContextBuilder> configure)
{
Guard.ThrowIfNull(builder);
Guard.ThrowIfNull(configure);

return builder.AddContext((b, _) => configure(b));
}

/// <summary>
/// This method is used to add a new context to the service collection.
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="configure">the desired configuration</param>
/// <returns>The <see cref="OpenFeatureBuilder"/> instance.</returns>
public static OpenFeatureBuilder AddContext(
this OpenFeatureBuilder builder,
Action<EvaluationContextBuilder, IServiceProvider> configure)
{
Guard.ThrowIfNull(builder);
Guard.ThrowIfNull(configure);

builder.IsContextConfigured = true;
builder.Services.TryAddTransient(provider =>
{
var contextBuilder = EvaluationContext.Builder();
configure(contextBuilder, provider);
return contextBuilder.Build();
});

return builder;
}

/// <summary>
/// Adds a feature provider to the service collection.
/// </summary>
/// <typeparam name="T">The type of the feature provider, which must inherit from <see cref="FeatureProvider"/>.</typeparam>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="providerFactory">A factory method to create the feature provider, using the service provider.</param>
/// <returns>The <see cref="OpenFeatureBuilder"/> instance.</returns>
public static OpenFeatureBuilder AddProvider<T>(this OpenFeatureBuilder builder, Func<IServiceProvider, T> providerFactory)
where T : FeatureProvider
{
Guard.ThrowIfNull(builder);
builder.Services.TryAddSingleton<FeatureProvider>(providerFactory);
return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenFeature.Internal;
using OpenFeature.Model;

namespace OpenFeature;

/// <summary>
/// Contains extension methods for the <see cref="IServiceCollection"/> class.
/// </summary>
public static class OpenFeatureServiceCollectionExtensions
{
/// <summary>
/// This method is used to add OpenFeature to the service collection.
/// OpenFeature will be registered as a singleton.
/// </summary>
/// <param name="services"><see cref="IServiceCollection"/></param>
/// <param name="configure">the desired configuration</param>
/// <returns>the current <see cref="IServiceCollection"/> instance</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action<OpenFeatureBuilder> configure)
{
Guard.ThrowIfNull(services);
Guard.ThrowIfNull(configure);

services.TryAddSingleton(Api.Instance);
services.TryAddSingleton<IFeatureLifecycleManager, FeatureLifecycleManager>();

Copy link

@wwalendz-relativity wwalendz-relativity Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having branching in all places where we would like to consume ExecutionContext we could always register empty instance of ExecutionContext. This will end up with more "pure" methods. In that case IsContextConfigured is not needed.
services.TryAddSingleton(ExecutionContext.Empty);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t register the EvaluationContext as a singleton because the client can operate without any context or with null. Under the hood, it checks and sets the Empty context as needed. The purpose of IsContextConfigured is to avoid redundant service resolution and nullability checks. If no context is registered, we bypass fetching the EvaluationContext entirely, preventing unnecessary GetService or GetRequiredService calls.

Also, when the context is registered, I register it as transient. The reason for this is to control its lifecycle within the scope of the class where it’s used—in this case, IFeatureClient. Will be confusing to register the context as transient in one case and Empty as singleton in another. However, this approach ensures proper lifecycle management based on the context's availability and usage.

Additionally, the IsContextConfigured check is only performed once during the service provider building process. In scenarios where context resolution isn’t required, the IFeatureClient can still function without a context, but engineers can always explicitly use ExecutionContext.Empty if needed

var builder = new OpenFeatureBuilder(services);
configure(builder);

if (builder.IsContextConfigured)
{
services.TryAddScoped<IFeatureClient>(static provider =>
{
var api = provider.GetRequiredService<Api>();
var client = api.GetClient();
var context = provider.GetRequiredService<EvaluationContext>();
client.SetContext(context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context can be added at both the global and client level. Global context is most useful for static values (for example, a value representing the timezone of the server or the cloud provider it's running on, or the application version).

We might need multiple contexts setting methods for the different scopes: global, client, and perhaps transaction level once we implement the transaction propagation feature. As it is, I'm not sure it's obvious to a user which one would be used when they do AddContext(...)

return client;
});
}
else
{
services.TryAddScoped<IFeatureClient>(static provider =>
{
var api = provider.GetRequiredService<Api>();
return api.GetClient();
});
}

return services;
}
}
Loading