From 6b69463ddbad1a0c8117ab0b2da54c7a03913a17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:22:43 +0000 Subject: [PATCH 01/14] chore(deps): update dependency nsubstitute to 5.3.0 (#311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [NSubstitute](https://nsubstitute.github.io/) ([source](https://redirect.github.com/nsubstitute/NSubstitute)) | `5.1.0` -> `5.3.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/NSubstitute/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/NSubstitute/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/NSubstitute/5.1.0/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/NSubstitute/5.1.0/5.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
nsubstitute/NSubstitute (NSubstitute) ### [`v5.3.0`](https://redirect.github.com/nsubstitute/NSubstitute/blob/HEAD/CHANGELOG.md#530-October-2024) - \[NEW] Introduced `Substitute.ForTypeForwardingTo` to create substitutes that forward interceptable calls to a concrete class. This provides an easy way of implementing a test spy over an existing type. Designed and implemented by [@​marcoregueira](https://redirect.github.com/marcoregueira) in [https://github.com/nsubstitute/NSubstitute/pull/700](https://redirect.github.com/nsubstitute/NSubstitute/pull/700) from a proposal by [@​wsaeed](https://redirect.github.com/wsaeed). Thanks to all who contributed to discussions of this feature. - \[NEW] Support Raise.EventWith default constructor ([#​788](https://redirect.github.com/nsubstitute/NSubstitute/issues/788)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/813](https://redirect.github.com/nsubstitute/NSubstitute/pull/813) - \[NEW] Implement When(...).Throws to avoid confusion with Throw method ([#​803](https://redirect.github.com/nsubstitute/NSubstitute/issues/803)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/818](https://redirect.github.com/nsubstitute/NSubstitute/pull/818) - \[FIX] Arg.Any\() does not match arguments passed by reference ([#​787](https://redirect.github.com/nsubstitute/NSubstitute/issues/787)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/811](https://redirect.github.com/nsubstitute/NSubstitute/pull/811) - \[FIX] Support matching arguments whose type is generic, when their concrete type is not known ([#​786](https://redirect.github.com/nsubstitute/NSubstitute/issues/786)) by [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) in [https://github.com/nsubstitute/NSubstitute/pull/814](https://redirect.github.com/nsubstitute/NSubstitute/pull/814) - \[FIX] Release build workflow [https://github.com/nsubstitute/NSubstitute/pull/797](https://redirect.github.com/nsubstitute/NSubstitute/pull/797)7) - \[DOC] Add Throws for exceptions to the docs by [@​304NotModified](https://redirect.github.com/304NotModified) in [https://github.com/nsubstitute/NSubstitute/pull/795](https://redirect.github.com/nsubstitute/NSubstitute/pull/795) - \[DOC] Remove Visual Studio for Mac from readme by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/807](https://redirect.github.com/nsubstitute/NSubstitute/pull/807) - \[TECH] Migrate from NUnit 3 to NUnit 4 by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/783](https://redirect.github.com/nsubstitute/NSubstitute/pull/783) - \[TECH] Update build project to .net 8 by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/776](https://redirect.github.com/nsubstitute/NSubstitute/pull/776) - \[TECH] Code style: use C# 12 collection literals by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/810](https://redirect.github.com/nsubstitute/NSubstitute/pull/810) - \[TECH] Use c# 12 primary constructors by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/812](https://redirect.github.com/nsubstitute/NSubstitute/pull/812) - \[TECH] Added csharp_style_prefer_primary_constructors into editorconfig by [@​Romfos](https://redirect.github.com/Romfos) in [https://github.com/nsubstitute/NSubstitute/pull/819](https://redirect.github.com/nsubstitute/NSubstitute/pull/819) Thanks to first-time contributors [@​mihnea-radulescu](https://redirect.github.com/mihnea-radulescu) and [@​marcoregueira](https://redirect.github.com/marcoregueira)! Thanks also [@​304NotModified](https://redirect.github.com/304NotModified) and [@​Romfos](https://redirect.github.com/Romfos) for their continued support and contributions to this release.
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e9358823..ef5cde51 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From 9db0bcf963ea27b3515ca5db2819ec772d5e1463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:55:06 +0000 Subject: [PATCH 02/14] fix: Fix action syntax in workflow configuration (#315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/code-coverage.yml | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 8052e6a3..58e74f1f 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -2,19 +2,19 @@ name: Code Coverage on: push: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" pull_request: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" jobs: build-test-report: strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -23,22 +23,22 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 8.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: | + 6.0.x + 8.0.x + source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.6.0 - with: - name: Code Coverage for ${{ matrix.os }} - fail_ci_if_error: true - verbose: true - token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} + - uses: codecov/codecov-action@v4.6.0 + with: + name: Code Coverage for ${{ matrix.os }} + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} From feac23ae0f8f048707c92f4c9a073dafea6f8741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:43:18 +0000 Subject: [PATCH 03/14] Bump sdk version. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- global.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 0ee5ec22..164f1fd1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { - "rollForward": "latestFeature", - "version": "8.0.404" + "rollForward": "latestMajor", + "version": "9.0.0", + "allowPrerelease": false } } From 326bbb475880a07fcfbc2696f2ce43a6454e13eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:44:14 +0000 Subject: [PATCH 04/14] Changed for vs code tasks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .vscode/launch.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4d46e206..c530628b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/test/OpenFeature.Tests/bin/Debug/net6.0/OpenFeature.Tests.dll", + "program": "${workspaceFolder}/test/OpenFeature.Tests/bin/Debug/net9.0/OpenFeature.Tests.dll", "args": [], "cwd": "${workspaceFolder}/test/OpenFeature.Tests", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console @@ -23,4 +23,4 @@ "request": "attach" } ] -} \ No newline at end of file +} From 24b59815ac4895abc3b5442c7d22defd189597ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:46:42 +0000 Subject: [PATCH 05/14] Added net9 target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/OpenFeature.csproj | 2 +- test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj | 2 +- test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj | 2 +- test/OpenFeature.Tests/OpenFeature.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index ed991c4e..abcdee11 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0;net8.0;net462 + net6.0;net8.0;net9.0;netstandard2.0;net462 OpenFeature README.md diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj index 974dce5c..92193210 100644 --- a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net6.0;net8.0;net9.0 OpenFeature.Benchmark Exe diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index d91b338e..9ea324a1 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net6.0;net8.0;net9.0 $(TargetFrameworks);net462 OpenFeature.E2ETests diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index bfadbf9b..35425feb 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net6.0;net8.0;net9.0 $(TargetFrameworks);net462 OpenFeature.Tests From 1d76f294f0ad0251ea101dd0e6ab23c94476e841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:50:18 +0000 Subject: [PATCH 06/14] Bump packages. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ef5cde51..16dd9b2b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,10 +5,10 @@ - - - - + + + + From 150e6fdd1345d6c3c3c7ff64c72893ce9ef313bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:51:34 +0000 Subject: [PATCH 07/14] Removed net6. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/OpenFeature.csproj | 2 +- test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj | 2 +- test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj | 2 +- test/OpenFeature.Tests/OpenFeature.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index abcdee11..357d39c5 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0;netstandard2.0;net462 + net8.0;net9.0;netstandard2.0;net462 OpenFeature README.md diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj index 92193210..c0dc300a 100644 --- a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0 OpenFeature.Benchmark Exe diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 9ea324a1..50cf1a6a 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0 $(TargetFrameworks);net462 OpenFeature.E2ETests diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 35425feb..4df0c681 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0 $(TargetFrameworks);net462 OpenFeature.Tests From 445d2d4ec63ab8bb13ed04ab1256ab05e4906ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:58:56 +0000 Subject: [PATCH 08/14] Upgraded actions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acd6d428..400687fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore @@ -66,8 +66,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Restore diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 58e74f1f..976cf5a2 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -29,8 +29,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Run Test diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 9af0ae8b..a6d5e36e 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: dotnet format run: dotnet format --verify-no-changes OpenFeature.sln diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 914d6809..0be1db57 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -24,8 +24,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Initialize Tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b51c9bff..574aa6ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,8 +36,8 @@ jobs: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies From 5856ed804a5b7170d0c1216a2017397f725b8959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:20:37 +0000 Subject: [PATCH 09/14] Changing README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b8f25012..e0e6e4cb 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ ### Requirements -- .NET 6+ -- .NET Core 6+ +- .NET 8+ - .NET Framework 4.6.2+ Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 From f616ae5fb27545546da3aae4405f6abefeef5047 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 18 Nov 2024 18:27:54 +0400 Subject: [PATCH 10/14] feat: Add Dependency Injection and Hosting support for OpenFeature (#310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!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:** ```bash dotnet add package OpenFeature dotnet add package OpenFeature.DependencyInjection dotnet add package OpenFeature.Hosting ``` --- **Usage Example:** ```csharp 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", ""); }) // Example of a feature provider configuration // .AddLaunchDarkly(builder.Configuration["LaunchDarkly:SdkKey"], cfg => cfg.StartWaitTime(TimeSpan.FromSeconds(10))); }); ``` Signed-off-by: Artyom Tonoyan Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> # Conflicts: # Directory.Packages.props --- Directory.Packages.props | 2 + OpenFeature.sln | 35 ++- README.md | 77 ++++- .../Diagnostics/FeatureCodes.cs | 38 +++ src/OpenFeature.DependencyInjection/Guard.cs | 20 ++ .../IFeatureLifecycleManager.cs | 24 ++ .../IFeatureProviderFactory.cs | 23 ++ .../Internal/FeatureLifecycleManager.cs | 51 ++++ .../CallerArgumentExpressionAttribute.cs | 23 ++ .../MultiTarget/IsExternalInit.cs | 21 ++ .../OpenFeature.DependencyInjection.csproj | 28 ++ .../OpenFeatureBuilder.cs | 60 ++++ .../OpenFeatureBuilderExtensions.cs | 280 ++++++++++++++++++ .../OpenFeatureOptions.cs | 49 +++ .../OpenFeatureServiceCollectionExtensions.cs | 68 +++++ .../PolicyNameOptions.cs | 12 + .../Memory/FeatureBuilderExtensions.cs | 55 ++++ .../Memory/InMemoryProviderFactory.cs | 34 +++ .../FeatureLifecycleStateOptions.cs | 18 ++ src/OpenFeature.Hosting/FeatureStartState.cs | 22 ++ src/OpenFeature.Hosting/FeatureStopState.cs | 22 ++ .../HostedFeatureLifecycleService.cs | 100 +++++++ .../OpenFeature.Hosting.csproj | 18 ++ .../OpenFeatureBuilderExtensions.cs | 38 +++ .../FeatureLifecycleManagerTests.cs | 64 ++++ .../NoOpFeatureProvider.cs | 52 ++++ .../NoOpFeatureProviderFactory.cs | 9 + .../NoOpProvider.cs | 8 + ...enFeature.DependencyInjection.Tests.csproj | 37 +++ .../OpenFeatureBuilderExtensionsTests.cs | 98 ++++++ ...FeatureServiceCollectionExtensionsTests.cs | 39 +++ 31 files changed, 1417 insertions(+), 8 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs create mode 100644 src/OpenFeature.DependencyInjection/Guard.cs create mode 100644 src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs create mode 100644 src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/PolicyNameOptions.cs create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs create mode 100644 src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs create mode 100644 src/OpenFeature.Hosting/FeatureStartState.cs create mode 100644 src/OpenFeature.Hosting/FeatureStopState.cs create mode 100644 src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs create mode 100644 src/OpenFeature.Hosting/OpenFeature.Hosting.csproj create mode 100644 src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 16dd9b2b..4b7e725a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + @@ -26,6 +27,7 @@ + diff --git a/OpenFeature.sln b/OpenFeature.sln index 6f1cce8d..e8191acd 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -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 @@ -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} diff --git a/README.md b/README.md index e0e6e4cb..3bfadecd 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,9 @@ public async Task Example() | ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | | ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| 🔬 | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. | -> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ +> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ | Experimental: 🔬 ### Providers @@ -299,6 +300,80 @@ public class MyHook : Hook Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! +### DependencyInjection +> [!NOTE] +> The OpenFeature.DependencyInjection and OpenFeature.Hosting packages are currently experimental. They streamline the integration of OpenFeature within .NET applications, allowing for seamless configuration and lifecycle management of feature flag providers using dependency injection and hosting services. + +#### Installation +To set up dependency injection and hosting capabilities for OpenFeature, install the following packages: +```sh +dotnet add package OpenFeature.DependencyInjection +dotnet add package OpenFeature.Hosting +``` +#### Usage Examples +For a basic configuration, you can use the InMemoryProvider. This provider is simple and well-suited for development and testing purposes. + +**Basic Configuration:** +```csharp +builder.Services.AddOpenFeature(featureBuilder => { + featureBuilder + .AddHostedFeatureLifecycle() // From Hosting package + .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) + .AddInMemoryProvider(); +}); +``` +**Domain-Scoped Provider Configuration:** +
To set up multiple providers with a selection policy, define logic for choosing the default provider. This example designates `name1` as the default provider: +```csharp +builder.Services.AddOpenFeature(featureBuilder => { + featureBuilder + .AddHostedFeatureLifecycle() + .AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ }) + .AddInMemoryProvider("name1") + .AddInMemoryProvider("name2") + .AddPolicyName(options => { + // Custom logic to select a default provider + options.DefaultNameSelector = serviceProvider => "name1"; + }); +}); +``` +#### Creating a New Provider +To integrate a custom provider, such as InMemoryProvider, you’ll need to create a factory that builds and configures the provider. This section demonstrates how to set up InMemoryProvider as a new provider with custom configuration options. + +**Configuring InMemoryProvider as a New Provider** +
Begin by creating a custom factory class, `InMemoryProviderFactory`, that implements `IFeatureProviderFactory`. This factory will initialize your provider with any necessary configurations. +```csharp +public class InMemoryProviderFactory : IFeatureProviderFactory +{ + internal IDictionary? Flags { get; set; } + + public FeatureProvider Create() => new InMemoryProvider(Flags); +} +``` +**Adding an Extension Method to OpenFeatureBuilder** +
To streamline the configuration process, add an extension method, `AddInMemoryProvider`, to `OpenFeatureBuilder`. This allows you to set up the provider with either a domain-scoped or a default configuration. + +```csharp +public static partial class FeatureBuilderExtensions +{ + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) + => builder.AddProvider(factory => ConfigureFlags(factory, configure)); + + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) + => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); + + private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) + { + if (configure == null) + return; + + var flag = new Dictionary(); + configure.Invoke(flag); + factory.Flags = flag; + } +} +``` + ## ⭐️ Support the project diff --git a/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs b/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs new file mode 100644 index 00000000..582ab39c --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs @@ -0,0 +1,38 @@ +namespace OpenFeature.DependencyInjection.Diagnostics; + +/// +/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework. +/// +/// +/// Experimental - 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. +/// +/// Basic Information
+/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize +/// and manage experimental features effectively. +///
+///
+/// +/// +/// Code Structure: +/// - "OF" - Represents the OpenFeature library. +/// - "DI" - Indicates the Dependency Injection domain. +/// - "001" - Unique identifier for a specific feature. +/// +/// +internal static class FeatureCodes +{ + /// + /// Identifier for the experimental Dependency Injection features within the OpenFeature framework. + /// + /// + /// OFDI001 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. + /// + public const string NewDi = "OFDI001"; +} diff --git a/src/OpenFeature.DependencyInjection/Guard.cs b/src/OpenFeature.DependencyInjection/Guard.cs new file mode 100644 index 00000000..337a8290 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Guard.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace OpenFeature.DependencyInjection; + +[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); + } + + public static void ThrowIfNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (string.IsNullOrWhiteSpace(argument)) + throw new ArgumentNullException(paramName); + } +} diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs new file mode 100644 index 00000000..4891f2e8 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs @@ -0,0 +1,24 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Defines the contract for managing the lifecycle of a feature api. +/// +public interface IFeatureLifecycleManager +{ + /// + /// 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. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of initializing the feature provider. + /// Thrown when the feature provider is not registered or is in an invalid state. + ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default); + + /// + /// 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. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of shutting down the feature provider. + ValueTask ShutdownAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs new file mode 100644 index 00000000..8c40cee3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs @@ -0,0 +1,23 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Provides a contract for creating instances of . +/// This factory interface enables custom configuration and initialization of feature providers +/// to support domain-specific or application-specific feature flag management. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public interface IFeatureProviderFactory +{ + /// + /// Creates an instance of a configured according to + /// the specific settings implemented by the concrete factory. + /// + /// + /// A new instance of . + /// The configuration and behavior of this provider instance are determined by + /// the implementation of this method. + /// + FeatureProvider Create(); +} diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs new file mode 100644 index 00000000..d14d421b --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenFeature.DependencyInjection.Internal; + +internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager +{ + private readonly Api _featureApi; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger logger) + { + _featureApi = featureApi; + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + this.LogStartingInitializationOfFeatureProvider(); + + var options = _serviceProvider.GetRequiredService>().Value; + if (options.HasDefaultProvider) + { + var featureProvider = _serviceProvider.GetRequiredService(); + await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); + } + + foreach (var name in options.ProviderNames) + { + var featureProvider = _serviceProvider.GetRequiredKeyedService(name); + await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false); + } + } + + /// + 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(); +} diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..afbec6b0 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs @@ -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 diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs new file mode 100644 index 00000000..87714111 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs @@ -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; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +static class IsExternalInit { } +#endif diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj new file mode 100644 index 00000000..895c45f3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;net6.0;net8.0;net462 + enable + enable + OpenFeature.DependencyInjection + + + + + + + + + + + + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs new file mode 100644 index 00000000..ae1e8c8f --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature.DependencyInjection; + +/// +/// Describes a backed by an . +/// +/// The services being configured. +public class OpenFeatureBuilder(IServiceCollection services) +{ + /// The services being configured. + public IServiceCollection Services { get; } = services; + + /// + /// 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. + /// + public bool IsContextConfigured { get; internal set; } + + /// + /// Indicates whether the policy has been configured. + /// + public bool IsPolicyConfigured { get; internal set; } + + /// + /// Gets a value indicating whether a default provider has been registered. + /// + public bool HasDefaultProvider { get; internal set; } + + /// + /// Gets the count of domain-bound providers that have been registered. + /// This count does not include the default provider. + /// + public int DomainBoundProviderRegistrationCount { get; internal set; } + + /// + /// 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. + /// + /// + /// 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. + /// + 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."); + } + } + } +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..a494b045 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,280 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; +using OpenFeature.Model; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(DependencyInjection.Diagnostics.FeatureCodes.NewDi)] +#endif +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// This method is used to add a new context to the service collection. + /// + /// The instance. + /// the desired configuration + /// The instance. + /// Thrown when the or action is null. + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); + + return builder.AddContext((b, _) => configure(b)); + } + + /// + /// This method is used to add a new context to the service collection. + /// + /// The instance. + /// the desired configuration + /// The instance. + /// Thrown when the or action is null. + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action 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; + } + + /// + /// Adds a new feature provider with specified options and configuration builder. + /// + /// The type for configuring the feature provider. + /// The type of the provider factory implementing . + /// The instance. + /// An optional action to configure the provider factory of type . + /// The instance. + /// Thrown when the is null. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) + where TOptions : OpenFeatureOptions + where TProviderFactory : class, IFeatureProviderFactory + { + Guard.ThrowIfNull(builder); + + builder.HasDefaultProvider = true; + + builder.Services.Configure(options => + { + options.AddDefaultProviderName(); + }); + + if (configureFactory != null) + { + builder.Services.AddOptions() + .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") + .Configure(configureFactory); + } + else + { + builder.Services.AddOptions() + .Configure(options => { }); + } + + builder.Services.TryAddSingleton(static provider => + { + var providerFactory = provider.GetRequiredService>().Value; + return providerFactory.Create(); + }); + + builder.AddClient(); + + return builder; + } + + /// + /// Adds a new feature provider with the default type and a specified configuration builder. + /// + /// The type of the provider factory implementing . + /// The instance. + /// An optional action to configure the provider factory of type . + /// The configured instance. + /// Thrown when the is null. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) + where TProviderFactory : class, IFeatureProviderFactory + => AddProvider(builder, configureFactory); + + /// + /// Adds a feature provider with specified options and configuration builder for the specified domain. + /// + /// The type for configuring the feature provider. + /// The type of the provider factory implementing . + /// The instance. + /// The unique name of the provider. + /// An optional action to configure the provider factory of type . + /// The instance. + /// Thrown when the or is null or empty. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) + where TOptions : OpenFeatureOptions + where TProviderFactory : class, IFeatureProviderFactory + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain)); + + builder.DomainBoundProviderRegistrationCount++; + + builder.Services.Configure(options => + { + options.AddProviderName(domain); + }); + + if (configureFactory != null) + { + builder.Services.AddOptions(domain) + .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") + .Configure(configureFactory); + } + else + { + builder.Services.AddOptions(domain) + .Configure(options => { }); + } + + builder.Services.TryAddKeyedSingleton(domain, static (provider, key) => + { + var options = provider.GetRequiredService>(); + var providerFactory = options.Get(key!.ToString()); + return providerFactory.Create(); + }); + + builder.AddClient(domain); + + return builder; + } + + /// + /// Adds a feature provider with a specified configuration builder for the specified domain, using default . + /// + /// The type of the provider factory implementing . + /// The instance. + /// The unique domain of the provider. + /// An optional action to configure the provider factory of type . + /// The configured instance. + /// Thrown when the or is null or empty. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) + where TProviderFactory : class, IFeatureProviderFactory + => AddProvider(builder, domain, configureFactory); + + /// + /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. + /// + /// The instance. + /// Optional: The name for the feature client instance. + /// The instance. + internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, string? name = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + if (builder.IsContextConfigured) + { + builder.Services.TryAddScoped(static provider => + { + var api = provider.GetRequiredService(); + var client = api.GetClient(); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + builder.Services.TryAddScoped(static provider => + { + var api = provider.GetRequiredService(); + return api.GetClient(); + }); + } + } + else + { + if (builder.IsContextConfigured) + { + builder.Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + var client = api.GetClient(key!.ToString()); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + builder.Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + return api.GetClient(key!.ToString()); + }); + } + } + + return builder; + } + + /// + /// Configures a default client for OpenFeature using the provided factory function. + /// + /// The instance. + /// + /// A factory function that creates an based on the service provider and . + /// + /// The configured instance. + internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory) + { + builder.Services.AddScoped(provider => + { + var policy = provider.GetRequiredService>().Value; + return clientFactory(provider, policy); + }); + + return builder; + } + + /// + /// Configures policy name options for OpenFeature using the specified options type. + /// + /// The type of options used to configure . + /// The instance. + /// A delegate to configure . + /// The configured instance. + /// Thrown when the or is null. + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + where TOptions : PolicyNameOptions + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configureOptions); + + builder.IsPolicyConfigured = true; + + builder.Services.Configure(configureOptions); + return builder; + } + + /// + /// Configures the default policy name options for OpenFeature. + /// + /// The instance. + /// A delegate to configure . + /// The configured instance. + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + => AddPolicyName(builder, configureOptions); +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs new file mode 100644 index 00000000..1be312ed --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -0,0 +1,49 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Options to configure OpenFeature +/// +public class OpenFeatureOptions +{ + private readonly HashSet _providerNames = []; + + /// + /// Determines if a default provider has been registered. + /// + public bool HasDefaultProvider { get; private set; } + + /// + /// The type of the configured feature provider. + /// + public Type FeatureProviderType { get; protected internal set; } = null!; + + /// + /// Gets a read-only list of registered provider names. + /// + public IReadOnlyCollection ProviderNames => _providerNames; + + /// + /// Registers the default provider name if no specific name is provided. + /// Sets to true. + /// + public void AddDefaultProviderName() => AddProviderName(null); + + /// + /// Registers a new feature provider name. This operation is thread-safe. + /// + /// The name of the feature provider to register. Registers as default if null. + public void AddProviderName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + HasDefaultProvider = true; + } + else + { + lock (_providerNames) + { + _providerNames.Add(name!); + } + } + } +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs new file mode 100644 index 00000000..e7a503bb --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; +using OpenFeature.DependencyInjection.Internal; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +public static partial class OpenFeatureServiceCollectionExtensions +{ + /// + /// Adds and configures OpenFeature services to the provided . + /// + /// The instance. + /// A configuration action for customizing OpenFeature setup via + /// The modified instance + /// Thrown if or is null. + public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) + { + Guard.ThrowIfNull(services); + Guard.ThrowIfNull(configure); + + // Register core OpenFeature services as singletons. + services.TryAddSingleton(Api.Instance); + services.TryAddSingleton(); + + var builder = new OpenFeatureBuilder(services); + configure(builder); + + // If a default provider is specified without additional providers, + // return early as no extra configuration is needed. + if (builder.HasDefaultProvider && builder.DomainBoundProviderRegistrationCount == 0) + { + return services; + } + + // Validate builder configuration to ensure consistency and required setup. + builder.Validate(); + + if (!builder.IsPolicyConfigured) + { + // Add a default name selector policy to use the first registered provider name as the default. + builder.AddPolicyName(options => + { + options.DefaultNameSelector = provider => + { + var options = provider.GetRequiredService>().Value; + return options.ProviderNames.First(); + }; + }); + } + + builder.AddDefaultClient((provider, policy) => + { + var name = policy.DefaultNameSelector.Invoke(provider); + if (name == null) + { + return provider.GetRequiredService(); + } + return provider.GetRequiredKeyedService(name); + }); + + return services; + } +} diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs new file mode 100644 index 00000000..f77b019b --- /dev/null +++ b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs @@ -0,0 +1,12 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Options to configure the default feature client name. +/// +public class PolicyNameOptions +{ + /// + /// A delegate to select the default feature client name. + /// + public Func DefaultNameSelector { get; set; } = null!; +} diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs new file mode 100644 index 00000000..199e01b0 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs @@ -0,0 +1,55 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// Extension methods for configuring feature providers with . +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public static partial class FeatureBuilderExtensions +{ + /// + /// Adds an in-memory feature provider to the with optional flag configuration. + /// + /// The instance to configure. + /// + /// An optional delegate to configure feature flags in the in-memory provider. + /// If provided, it allows setting up the initial flags. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) + => builder.AddProvider(factory => ConfigureFlags(factory, configure)); + + /// + /// Adds an in-memory feature provider with a specific domain to the + /// with optional flag configuration. + /// + /// The instance to configure. + /// The unique domain of the provider + /// + /// An optional delegate to configure feature flags in the in-memory provider. + /// If provided, it allows setting up the initial flags. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) + => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); + + /// + /// Configures the feature flags for an instance. + /// + /// The to configure. + /// + /// An optional delegate that sets up the initial flags in the provider's flag dictionary. + /// + private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) + { + if (configure == null) + return; + + var flag = new Dictionary(); + configure.Invoke(flag); + factory.Flags = flag; + } +} diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs new file mode 100644 index 00000000..2d155dd9 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs @@ -0,0 +1,34 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// A factory for creating instances of , +/// an in-memory implementation of . +/// This factory allows for the customization of feature flags to facilitate +/// testing and lightweight feature flag management without external dependencies. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public class InMemoryProviderFactory : IFeatureProviderFactory +{ + /// + /// Gets or sets the collection of feature flags used to configure the + /// instances. This dictionary maps + /// flag names to instances, enabling pre-configuration + /// of features for testing or in-memory evaluation. + /// + internal IDictionary? Flags { get; set; } + + /// + /// Creates a new instance of with the specified + /// flags set in . This instance is configured for in-memory + /// feature flag management, suitable for testing or lightweight feature toggling scenarios. + /// + /// + /// A configured that can be used to manage + /// feature flags in an in-memory context. + /// + public FeatureProvider Create() => new InMemoryProvider(Flags); +} diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs new file mode 100644 index 00000000..91e3047d --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs @@ -0,0 +1,18 @@ +namespace OpenFeature; + +/// +/// Represents the lifecycle state options for a feature, +/// defining the states during the start and stop lifecycle. +/// +public class FeatureLifecycleStateOptions +{ + /// + /// Gets or sets the state during the feature startup lifecycle. + /// + public FeatureStartState StartState { get; set; } = FeatureStartState.Starting; + + /// + /// Gets or sets the state during the feature shutdown lifecycle. + /// + public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping; +} diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs new file mode 100644 index 00000000..8001b9c2 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStartState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for starting a feature. +/// +public enum FeatureStartState +{ + /// + /// The feature is in the process of starting. + /// + Starting, + + /// + /// The feature is at the start state. + /// + Start, + + /// + /// The feature has fully started. + /// + Started +} diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs new file mode 100644 index 00000000..d8d6a28c --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStopState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for stopping a feature. +/// +public enum FeatureStopState +{ + /// + /// The feature is in the process of stopping. + /// + Stopping, + + /// + /// The feature is at the stop state. + /// + Stop, + + /// + /// The feature has fully stopped. + /// + Stopped +} diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs new file mode 100644 index 00000000..5209a525 --- /dev/null +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; + +namespace OpenFeature.Hosting; + +/// +/// A hosted service that manages the lifecycle of features within the application. +/// It ensures that features are properly initialized when the service starts +/// and gracefully shuts down when the service stops. +/// +public sealed partial class HostedFeatureLifecycleService : IHostedLifecycleService +{ + private readonly ILogger _logger; + private readonly IFeatureLifecycleManager _featureLifecycleManager; + private readonly IOptions _featureLifecycleStateOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The logger used to log lifecycle events. + /// The feature lifecycle manager responsible for initialization and shutdown. + /// Options that define the start and stop states of the feature lifecycle. + public HostedFeatureLifecycleService( + ILogger logger, + IFeatureLifecycleManager featureLifecycleManager, + IOptions featureLifecycleStateOptions) + { + _logger = logger; + _featureLifecycleManager = featureLifecycleManager; + _featureLifecycleStateOptions = featureLifecycleStateOptions; + } + + /// + /// Ensures that the feature is properly initialized when the service starts. + /// + public async Task StartingAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Starting, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Start" state. + /// + public async Task StartAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Start, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully started and operational. + /// + public async Task StartedAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Started, cancellationToken).ConfigureAwait(false); + + /// + /// Gracefully shuts down the feature when the service is stopping. + /// + public async Task StoppingAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopping, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Stop" state. + /// + public async Task StopAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stop, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully stopped and no longer operational. + /// + public async Task StoppedAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopped, cancellationToken).ConfigureAwait(false); + + /// + /// Initializes the feature lifecycle if the current state matches the expected start state. + /// + private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StartState == expectedState) + { + this.LogInitializingFeatureLifecycleManager(expectedState); + await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Shuts down the feature lifecycle if the current state matches the expected stop state. + /// + private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StopState == expectedState) + { + this.LogShuttingDownFeatureLifecycleManager(expectedState); + await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false); + } + } + + [LoggerMessage(200, LogLevel.Information, "Initializing the Feature Lifecycle Manager for state {State}.")] + partial void LogInitializingFeatureLifecycleManager(FeatureStartState state); + + [LoggerMessage(200, LogLevel.Information, "Shutting down the Feature Lifecycle Manager for state {State}")] + partial void LogShuttingDownFeatureLifecycleManager(FeatureStopState state); +} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj new file mode 100644 index 00000000..48730084 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net8.0 + enable + enable + OpenFeature + + + + + + + + + + + diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..16f437b3 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.DependencyInjection; +using OpenFeature.Hosting; + +namespace OpenFeature; + +/// +/// Extension methods for configuring the hosted feature lifecycle in the . +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// Adds the to the OpenFeatureBuilder, + /// which manages the lifecycle of features within the application. It also allows + /// configuration of the . + /// + /// The instance. + /// An optional action to configure . + /// The instance. + public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) + { + if (configureOptions == null) + { + builder.Services.Configure(cfg => + { + cfg.StartState = FeatureStartState.Starting; + cfg.StopState = FeatureStopState.Stopping; + }); + } + else + { + builder.Services.Configure(configureOptions); + } + + builder.Services.AddHostedService(); + return builder; + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs new file mode 100644 index 00000000..b0176bc4 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using OpenFeature.DependencyInjection.Internal; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class FeatureLifecycleManagerTests +{ + private readonly FeatureLifecycleManager _systemUnderTest; + private readonly IServiceProvider _mockServiceProvider; + + public FeatureLifecycleManagerTests() + { + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + + _mockServiceProvider = Substitute.For(); + + var options = new OpenFeatureOptions(); + options.AddDefaultProviderName(); + var optionsMock = Substitute.For>(); + optionsMock.Value.Returns(options); + + _mockServiceProvider.GetService>().Returns(optionsMock); + + _systemUnderTest = new FeatureLifecycleManager( + Api.Instance, + _mockServiceProvider, + Substitute.For>()); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists() + { + // Arrange + var featureProvider = new NoOpFeatureProvider(); + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(featureProvider); + + // Act + await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true); + + // Assert + Api.Instance.GetProvider().Should().BeSameAs(featureProvider); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist() + { + // Arrange + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); + + // Act + var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); + + // Assert + var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true); + exception.Should().NotBeNull(); + exception.Message.Should().NotBeNullOrWhiteSpace(); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs new file mode 100644 index 00000000..ac3e5209 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs @@ -0,0 +1,52 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs. +// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class. +// If the InternalsVisibleTo attribute is added to the OpenFeature project, +// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing. +internal sealed class NoOpFeatureProvider : FeatureProvider +{ + private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); + + public override Metadata GetMetadata() + { + return this._metadata; + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) + { + return new ResolutionDetails( + flagKey, + defaultValue, + reason: NoOpProvider.ReasonNoOp, + variant: NoOpProvider.Variant + ); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs new file mode 100644 index 00000000..1ee14bf0 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs @@ -0,0 +1,9 @@ +namespace OpenFeature.DependencyInjection.Tests; + +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif +public class NoOpFeatureProviderFactory : IFeatureProviderFactory +{ + public FeatureProvider Create() => new NoOpFeatureProvider(); +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs new file mode 100644 index 00000000..7bf20bca --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs @@ -0,0 +1,8 @@ +namespace OpenFeature.DependencyInjection.Tests; + +internal static class NoOpProvider +{ + public const string NoOpProviderName = "No-op Provider"; + public const string ReasonNoOp = "No-op"; + public const string Variant = "No-op"; +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj new file mode 100644 index 00000000..9937e1bc --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0;net8.0 + enable + enable + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 00000000..3f6ef227 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public partial class OpenFeatureBuilderExtensionsTests +{ + private readonly IServiceCollection _services; + private readonly OpenFeatureBuilder _systemUnderTest; + + public OpenFeatureBuilderExtensionsTests() + { + _services = new ServiceCollection(); + _systemUnderTest = new OpenFeatureBuilder(_services); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) + { + // Act + var result = useServiceProviderDelegate ? + _systemUnderTest.AddContext(_ => { }) : + _systemUnderTest.AddContext((_, _) => { }); + + // Assert + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + _services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(EvaluationContext) && + serviceDescriptor.Lifetime == ServiceLifetime.Transient, + "A transient service of type EvaluationContext should be added."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) + { + // Arrange + bool delegateCalled = false; + + _ = useServiceProviderDelegate ? + _systemUnderTest.AddContext(_ => delegateCalled = true) : + _systemUnderTest.AddContext((_, _) => delegateCalled = true); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var context = serviceProvider.GetService(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + context.Should().NotBeNull("The EvaluationContext should be resolvable."); + delegateCalled.Should().BeTrue("The delegate should be invoked."); + } + +#if NET8_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif + [Fact] + public void AddProvider_ShouldAddProviderToCollection() + { + // Act + var result = _systemUnderTest.AddProvider(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(FeatureProvider) && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + "A singleton service of type FeatureProvider should be added."); + } + +#if NET8_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.Experimental(Diagnostics.FeatureCodes.NewDi)] +#endif + [Fact] + public void AddProvider_ShouldResolveCorrectProvider() + { + // Arrange + _systemUnderTest.AddProvider(); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetService(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..40e761d2 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class OpenFeatureServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _systemUnderTest; + private readonly Action _configureAction; + + public OpenFeatureServiceCollectionExtensionsTests() + { + _systemUnderTest = new ServiceCollection(); + _configureAction = Substitute.For>(); + } + + [Fact] + public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSingleton() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); + } + + [Fact] + public void AddOpenFeature_ShouldInvokeConfigureAction() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + // Assert + _configureAction.Received(1).Invoke(Arg.Any()); + } +} From d16b74a07b21fbd51736ac12ece8ad0d6f32e237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:03:56 +0000 Subject: [PATCH 11/14] Bump target framework. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeature.DependencyInjection.csproj | 2 +- src/OpenFeature.Hosting/OpenFeature.Hosting.csproj | 2 +- .../OpenFeature.DependencyInjection.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 895c45f3..bfc590fd 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0;net8.0;net462 + netstandard2.0;net8.0;net9.0;net462 enable enable OpenFeature.DependencyInjection diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj index 48730084..43237e0f 100644 --- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 enable enable OpenFeature diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj index 9937e1bc..e4c16ee5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 enable enable From 1282ac1beef8645f865cf704996830e20d8857dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:04:05 +0000 Subject: [PATCH 12/14] Bump AutoFixture. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4b7e725a..1a471f89 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@
- + From 360edf5cbdeb6145e4c055ac9c3a17dd12fcb4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:09:04 +0000 Subject: [PATCH 13/14] Update global.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 164f1fd1..a35b4984 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMajor", - "version": "9.0.0", + "version": "9.0.100", "allowPrerelease": false } } From 351685527912806255bfebed32edbffc5880b6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:24:10 +0000 Subject: [PATCH 14/14] Fix target framework. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeature.IntegrationTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj index 8287b2ec..13c2f21e 100644 --- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable