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 135 commits into
base: main
Choose a base branch
from

Conversation

arttonoyan
Copy link

Note

This initial version of OpenFeature.DependencyInjection and OpenFeature.Hosting introduces basic provider management and lifecycle support for .NET Dependency Injection environments. While I haven't included extensive tests, particularly for the Hosting project, if this approach is approved and the scope is considered sufficient for the first DI and Hosting release, I will expand the test coverage to include both unit and integration tests. Additionally, I'll provide a sample application to demonstrate the usage.

This pull request introduces key features and improvements to the OpenFeature project, focusing on Dependency Injection and Hosting support:

  • OpenFeature.DependencyInjection Project:

    • Implemented OpenFeatureBuilder, including OpenFeatureBuilderExtensions for seamless integration.
    • Added IFeatureLifecycleManager interface and its implementation.
    • Introduced AddProvider extension method for easy provider configuration.
    • Created OpenFeatureServiceCollectionExtensions for service registration.
  • OpenFeature.Hosting Project:

    • Added HostedFeatureLifecycleService to manage the lifecycle of feature providers in hosted environments.
  • Testing Enhancements:

    • Created unit tests for critical methods, including OpenFeatureBuilderExtensionsTests and OpenFeatureServiceCollectionExtensionsTests.
    • Replicated and tested NoOpFeatureProvider implementation for better test coverage.

These changes significantly improve OpenFeature's extensibility and lifecycle management for feature providers within Dependency Injection (DI) and hosted environments.


NuGet Packages for installation:

dotnet add package OpenFeature
dotnet add package OpenFeature.DependencyInjection
dotnet add package OpenFeature.Hosting

Usage Example:

builder.Services.AddOpenFeature(featureBuilder => {
    featureBuilder
        .AddHostedFeatureLifecycle() // From Hosting package
        .AddContext((context, serviceProvider) => {
            // Context settings are applied here. Each feature flag evaluation will
            // automatically have access to these context parameters.
            // Do something with the service provider.
            context
                .Set("kind", "tenant")
                .Set("key", "<some key>");
        })
        // Example of a feature provider configuration
        // .AddLaunchDarkly(builder.Configuration["LaunchDarkly:SdkKey"], cfg => cfg.StartWaitTime(TimeSpan.FromSeconds(10))); 
});

@arttonoyan arttonoyan requested a review from a team as a code owner October 13, 2024 19:12
@beeme1mr beeme1mr changed the title Add Dependency Injection and Hosting support for OpenFeature feat: Add Dependency Injection and Hosting support for OpenFeature Oct 13, 2024
@askpt askpt linked an issue Oct 14, 2024 that may be closed by this pull request
@askpt
Copy link
Member

askpt commented Oct 14, 2024

Hey @arttonoyan! The build is failing because of compliance. Could you please follow these steps? https://github.com/open-feature/dotnet-sdk/pull/310/checks?check_run_id=31479940982

@toddbaert
Copy link
Member

@arttonoyan regartding this comment, the main reason I wanted the DI namespace is because I really wanted to make it clear what is subject to change, and what is not, and putting those things in the DI namespace helped with that. However, if we add the Experimental decorator to all these new DI classes, that's good enough for me; I just want to make sure people know those features are experimental and subject to potential change going forward.

Copy link
Member

@askpt askpt left a comment

Choose a reason for hiding this comment

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

I am adding a couple of suggestions about the namespace.

using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenFeature.DependencyInjection;
using OpenFeature.Model;

namespace OpenFeature;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
namespace OpenFeature;
namespace OpenFeature.DependencyInjection;

using OpenFeature.Model;
using Microsoft.Extensions.Options;
using OpenFeature.DependencyInjection;
using OpenFeature.DependencyInjection.Internal;

namespace OpenFeature;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
namespace OpenFeature;
namespace OpenFeature.DependencyInjection;

src/OpenFeature.DependencyInjection/PolicyNameOptions.cs Outdated Show resolved Hide resolved
src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs Outdated Show resolved Hide resolved
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using OpenFeature.DependencyInjection;

namespace OpenFeature;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
namespace OpenFeature;
namespace OpenFeature.Hosting;

@arttonoyan
Copy link
Author

Thanks, @askpt, for the suggestion. Let me explain why I prefer not to change these namespaces:

Grouping OpenFeatureServiceCollectionExtensions and OpenFeatureBuilderExtensions under a single namespace (OpenFeature) across projects makes it easier for consumers to configure the OpenFeature package using just one namespace: OpenFeature. Otherwise, they would need to add multiple using statements in their Program.cs file, such as:

using OpenFeature.Hosting;
using OpenFeature.DependencyInjection;

As the project grows, this list could expand, increasing complexity. Keeping everything under one namespace simplifies configuration and improves the developer experience by reducing clutter and creating a cohesive setup process.

Additionally, this approach supports a clear, consistent API structure, making it easier to manage and recall namespaces.
However, if aligning namespaces with project names is a priority, I’m open to revisiting this.

@askpt
Copy link
Member

askpt commented Nov 1, 2024

Thanks, @askpt, for the suggestion. Let me explain why I prefer not to change these namespaces:

Grouping OpenFeatureServiceCollectionExtensions and OpenFeatureBuilderExtensions under a single namespace (OpenFeature) across projects makes it easier for consumers to configure the OpenFeature package using just one namespace: OpenFeature. Otherwise, they would need to add multiple using statements in their Program.cs file, such as:

using OpenFeature.Hosting;
using OpenFeature.DependencyInjection;

As the project grows, this list could expand, increasing complexity. Keeping everything under one namespace simplifies configuration and improves the developer experience by reducing clutter and creating a cohesive setup process.

Additionally, this approach supports a clear, consistent API structure, making it easier to manage and recall namespaces. However, if aligning namespaces with project names is a priority, I’m open to revisiting this.

I understand your point, but from a user who is consuming the "packages", it would be weird to be named "OpenFeature". I suggest changing the "package" name or grouping both into "OpenFeature.DependencyInjection" so it won't be confusing.

Imagine you are a new user using the OpenFeature.OpenFeatureServiceCollectionExtensions. If you have an issue, where would you search for information? The OpenFeature package or OpenFeature.Hosting?

Thank you so much for your work and input. If you need any help with the Experimental attribute, let me know and I can try help you with that 👍

@arttonoyan
Copy link
Author

Thanks, @askpt, for the suggestion. Let me explain why I prefer not to change these namespaces:
Grouping OpenFeatureServiceCollectionExtensions and OpenFeatureBuilderExtensions under a single namespace (OpenFeature) across projects makes it easier for consumers to configure the OpenFeature package using just one namespace: OpenFeature. Otherwise, they would need to add multiple using statements in their Program.cs file, such as:

using OpenFeature.Hosting;
using OpenFeature.DependencyInjection;

As the project grows, this list could expand, increasing complexity. Keeping everything under one namespace simplifies configuration and improves the developer experience by reducing clutter and creating a cohesive setup process.
Additionally, this approach supports a clear, consistent API structure, making it easier to manage and recall namespaces. However, if aligning namespaces with project names is a priority, I’m open to revisiting this.

I understand your point, but from a user who is consuming the "packages", it would be weird to be named "OpenFeature". I suggest changing the "package" name or grouping both into "OpenFeature.DependencyInjection" so it won't be confusing.

Imagine you are a new user using the OpenFeature.OpenFeatureServiceCollectionExtensions. If you have an issue, where would you search for information? The OpenFeature package or OpenFeature.Hosting?

Thank you so much for your work and input. If you need any help with the Experimental attribute, let me know and I can try help you with that 👍

I understand your perspective, and you’re right that intuitive package naming is important for new users. The approach I’m suggesting is actually similar to how Microsoft organizes core libraries - grouping related APIs in a way that prioritizes logical grouping over a strict alignment with package names. This doesn’t tend to complicate usage, as Visual Studio (or other IDEs) makes it easy to navigate by pressing F12 to go directly to code definitions, where the library and assembly information are clearly visible.

However, I also recognize the importance of clarity for new users. If you feel strongly that renaming or grouping under OpenFeature.DependencyInjection will better align expectations, I’ll change.

And @askpt, if you’re able to lend a hand with this as well, that would be great!

@toddbaert
Copy link
Member

@askpt to clarify, the intent was not to release a separate nuget artifact for these changes (we don't even have the publishing mechanisms in this repo to support that); the idea was just to add new functionality marked as experimental using the [Experimental] attribute to the current SDK. I hope that clarifies some of your namespace concerns.

@arttonoyan You should be able to annotate all the new classes or functions (basically anything a user would touch) like this:

#if !NET462
        [System.Diagnostics.CodeAnalysis.Experimental(Constants.NewDiFeatures)]
#endif

Where Constants.NewDiFeatures is a common string we use to identify all our experimental new DI stuff. Users can then disable the associated warning by ignoring this error for that diagnostic ID (meaning they won't get any warnings for using these new features)

Note that this attribute doesn't exist in .NET framework (462) so you need to add the prepreoceessor directive (#if !NET462) to disable it there; those users won't get the warning but .NET 462 users are probably a minority these days. Please also use the fully qualified attribute name (System.Diagnostics.CodeAnalysis.Experimental) as well (IMO that's a good habit for imports that are used conditionally).

@arttonoyan
Copy link
Author

@askpt to clarify, the intent was not to release a separate nuget artifact for these changes (we don't even have the publishing mechanisms in this repo to support that); the idea was just to add new functionality marked as experimental using the [Experimental] attribute to the current SDK. I hope that clarifies some of your namespace concerns.

@arttonoyan You should be able to annotate all the new classes or functions (basically anything a user would touch) like this:

#if !NET462
        [System.Diagnostics.CodeAnalysis.Experimental(Constants.NewDiFeatures)]
#endif

Where Constants.NewDiFeatures is a common string we use to identify all our experimental new DI stuff. Users can then disable the associated warning by ignoring this error for that diagnostic ID (meaning they won't get any warnings for using these new features)

Note that this attribute doesn't exist in .NET framework (462) so you need to add the prepreoceessor directive (#if !NET462) to disable it there; those users won't get the warning but .NET 462 users are probably a minority these days. Please also use the fully qualified attribute name (System.Diagnostics.CodeAnalysis.Experimental) as well (IMO that's a good habit for imports that are used conditionally).

Thanks, @toddbaert! I’m working on it, though it’s impacting quite a few classes. Another approach could be to build pre-release packages and mark the package itself as experimental, which aligns with NuGet's recommendations for experimental features: Pre-release Packages.

To make it clear for users, we could set the version <Version>1.0.0-experimental</Version>, marking it as pre-release on NuGet. Additionally, we could update the NuGet metadata to indicate the experimental status, for example:

<PackageReleaseNotes>This is an experimental version. Features may change without notice.</PackageReleaseNotes>
<Description>Experimental package for testing new features. Not recommended for production use.</Description>

This would help users recognize the package as experimental and avoid unintended use in production. What do you think?

@arttonoyan
Copy link
Author

@askpt to clarify, the intent was not to release a separate nuget artifact for these changes (we don't even have the publishing mechanisms in this repo to support that); the idea was just to add new functionality marked as experimental using the [Experimental] attribute to the current SDK. I hope that clarifies some of your namespace concerns.
@arttonoyan You should be able to annotate all the new classes or functions (basically anything a user would touch) like this:

#if !NET462
        [System.Diagnostics.CodeAnalysis.Experimental(Constants.NewDiFeatures)]
#endif

Where Constants.NewDiFeatures is a common string we use to identify all our experimental new DI stuff. Users can then disable the associated warning by ignoring this error for that diagnostic ID (meaning they won't get any warnings for using these new features)
Note that this attribute doesn't exist in .NET framework (462) so you need to add the prepreoceessor directive (#if !NET462) to disable it there; those users won't get the warning but .NET 462 users are probably a minority these days. Please also use the fully qualified attribute name (System.Diagnostics.CodeAnalysis.Experimental) as well (IMO that's a good habit for imports that are used conditionally).

Thanks, @toddbaert! I’m working on it, though it’s impacting quite a few classes. Another approach could be to build pre-release packages and mark the package itself as experimental, which aligns with NuGet's recommendations for experimental features: Pre-release Packages.

To make it clear for users, we could set the version <Version>1.0.0-experimental</Version>, marking it as pre-release on NuGet. Additionally, we could update the NuGet metadata to indicate the experimental status, for example:

<PackageReleaseNotes>This is an experimental version. Features may change without notice.</PackageReleaseNotes>
<Description>Experimental package for testing new features. Not recommended for production use.</Description>

This would help users recognize the package as experimental and avoid unintended use in production. What do you think?

@toddbaert I've created a new PR to add the Experimental attribute conditionally for .NET 8 and above, specifically for new DI features.

Key Points:

  • The attribute is applied conditionally using #if NET8_0_OR_GREATER, so it only works for .NET 8 and greater.
  • For .NET Framework 4.6.2 (or other frameworks that don’t match this condition), the attribute will not be applied. The conditional check #if !NET462 does not function as intended for targeting non-.NET 8 frameworks in this scenario.

I’ve created this change in a separate branch, as I think marking the package as experimental is more convenient, as mentioned above. However, the PR is open, and I'm open to feedback – please feel free to decide if this is the best approach for this package.
arttonoyan#2

@toddbaert
Copy link
Member

toddbaert commented Nov 4, 2024

@arttonoyan The problem is we dont want to mark the entire SDK as experimental, and these features will be included in all subsequent releases, so I don't think an experimental version number can help, unless we want to merge these features to a non-main branch and maintain that along side the main branch (I'm not in favor of this for the maintenance burden it creates).

If you can add PackageReleaseNotes to the new .csproj files, that's fine, but I would say "Features may change without notice" and avoid not recommending for production. I think we want to consider this features "production grade"; just changable. I also think that perhaps we should add the Experimental attibute to any of the DI-related classes just in the OpenFeature namespace, since it might not be clear they are coming from the experimental packages... we can avoid it for classes/artifacts in the new experimentally-marked packages/namespaces... does that make sense?

@arttonoyan
Copy link
Author

@toddbaert, @beeme1mr, @askpt and team!
I’ve submitted a PR to propose a structured approach for Diagnostic Feature Codes in OpenFeature, which aims to standardize the way we track and manage experimental features.
arttonoyan#3

This PR is a starting point and includes a proposed code format [Prefix][Domain][UniqueNumber] (e.g., OFDI001) that could serve as the foundation for future enhancements. I’ve referenced best practices from OpenTelemetry and EF Core, and I’m hoping it sparks a conversation on how we can formalize diagnostics within OpenFeature.

I know it’s a bit lengthy, but I wanted to make sure it’s thoughtful and not just rushed to completion. I’d appreciate it if you could take a few minutes to review the PR and share any thoughts or suggestions. Feedback on the code format, potential improvements, or alignment with OpenFeature’s objectives would be especially helpful.

@beeme1mr
Copy link
Member

beeme1mr commented Nov 5, 2024

Amazing, thanks. I'll take a look ASAP.

arttonoyan and others added 2 commits November 9, 2024 15:06
…ttribute-v2

feat: Initial Proposal for Diagnostic Feature Codes in OpenFeature
Copy link
Member

@beeme1mr beeme1mr left a comment

Choose a reason for hiding this comment

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

Great, thank you. As a follow-up, could you please update the readme to include a section on how to configure DI?

Copy link
Member

Choose a reason for hiding this comment

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

This is a great addition.

Signed-off-by: Artyom Tonoyan <[email protected]>
@arttonoyan
Copy link
Author

@beeme1mr, @toddbaert, @askpt Team, I've updated the README file - please take a look when you have a moment. I've also added a new Experimental type with an icon (open to feedback on this, or we can use a predefined one).

@toddbaert toddbaert self-requested a review November 13, 2024 18:32
Copy link
Member

@toddbaert toddbaert left a comment

Choose a reason for hiding this comment

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

This looks good to me now. The packaging also seems to work well based on the CI test packaging, and since the experimental features here are in their own packages, I have no objections to releasing as-is.

Apart from my approval, you'll have to sign off your commits as described in the failing DCO check here. Alternatively, you can squash all this down into one commit and sign that. Either way is fine with me but this is a policy enforced by our parent organize, the CNCF.

I will merge this in the next couple days unless I hear objections from @kinyoklion @askpt @lukas-reining or @thomaspoignant

/// <summary>
/// This method is used to add a new context to the service collection.
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
Copy link
Member

Choose a reason for hiding this comment

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

nit: It would be nice to keep the style of parameter documentation consistent. I think that the prevalent style is currently fragments instead of sentences.

/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="configure">the desired configuration</param>
/// <returns>The <see cref="OpenFeatureBuilder"/> instance.</returns>
Copy link
Member

@kinyoklion kinyoklion Nov 13, 2024

Choose a reason for hiding this comment

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

Should we have exception documentation for the associated guards? (same comment applies to multiple functions)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Introduce OpenFeature.Extensions.Hosting package
8 participants