diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..2ec7a6cb2
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,33 @@
+{
+ "name": "OpenFeature .NET SDK",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
+ "features": {
+ "ghcr.io/devcontainers/features/dotnet:latest": {
+ "version": "10.0",
+ "dotnetRuntimeVersions": "9.0, 8.0"
+ },
+ "ghcr.io/devcontainers/features/github-cli:latest": {},
+ "ghcr.io/devcontainers/features/docker-in-docker": {}
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "EditorConfig.EditorConfig",
+ "GitHub.copilot",
+ "GitHub.copilot-chat",
+ "GitHub.vscode-github-actions",
+ "GitHub.vscode-pull-request-github",
+ "ms-dotnettools.csharp",
+ "esbenp.prettier-vscode",
+ "redhat.vscode-yaml",
+ "cucumberopen.cucumber-official",
+ "ms-dotnettools.csdevkit"
+ ]
+ }
+ },
+ "remoteUser": "vscode",
+ "hostRequirements": {
+ "memory": "8gb"
+ },
+ "postCreateCommand": "git submodule update --init --recursive"
+}
\ No newline at end of file
diff --git a/.github/actions/sbom-generator/action.yml b/.github/actions/sbom-generator/action.yml
index 7573150b5..9d27d486b 100644
--- a/.github/actions/sbom-generator/action.yml
+++ b/.github/actions/sbom-generator/action.yml
@@ -35,7 +35,7 @@ runs:
gh release upload ${{ inputs.release-tag }} ./artifacts/sboms/${{ inputs.project-name }}.bom.json
- name: Attest package
- uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b # v2.4.0
+ uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
with:
subject-path: src/**/${{ inputs.project-name }}.*.nupkg
sbom-path: ./artifacts/sboms/${{ inputs.project-name }}.bom.json
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml
new file mode 100644
index 000000000..4ba1698ce
--- /dev/null
+++ b/.github/workflows/aot-compatibility.yml
@@ -0,0 +1,96 @@
+name: AOT Compatibility
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ merge_group:
+ workflow_dispatch:
+
+jobs:
+ aot-compatibility:
+ name: AOT Test (${{ matrix.os }}, ${{ matrix.arch }})
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Linux x64
+ - os: ubuntu-latest
+ arch: x64
+ runtime: linux-x64
+ # Linux ARM64
+ - os: ubuntu-24.04-arm
+ arch: arm64
+ runtime: linux-arm64
+ # Windows x64
+ - os: windows-latest
+ arch: x64
+ runtime: win-x64
+ # Windows ARM64
+ - os: windows-11-arm
+ arch: arm64
+ runtime: win-arm64
+ # macOS x64
+ - os: macos-15-intel
+ arch: x64
+ runtime: osx-x64
+ # macOS ARM64 (Apple Silicon)
+ - os: macos-latest
+ arch: arm64
+ runtime: osx-arm64
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
+ with:
+ global-json-file: global.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ matrix.arch }}-nuget-
+ ${{ runner.os }}-nuget-
+
+ - name: Restore dependencies
+ shell: pwsh
+ run: dotnet restore
+
+ - name: Build solution
+ shell: pwsh
+ run: dotnet build -c Release --no-restore
+
+ - name: Test AOT compatibility project build
+ shell: pwsh
+ run: dotnet build test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj -c Release --no-restore
+
+ - name: Publish AOT compatibility test (cross-platform)
+ shell: pwsh
+ run: |
+ dotnet publish test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj `
+ -f net10.0 `
+ -r ${{ matrix.runtime }} `
+ -o ./aot-output
+
+ - name: Run AOT compatibility test
+ shell: pwsh
+ run: |
+ if ("${{ runner.os }}" -eq "Windows") {
+ ./aot-output/OpenFeature.AotCompatibility.exe
+ } else {
+ chmod +x ./aot-output/OpenFeature.AotCompatibility
+ ./aot-output/OpenFeature.AotCompatibility
+ }
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bb1c72273..d92c7a849 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,18 +23,23 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
submodules: recursive
- name: Setup .NET SDK
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
with:
global-json-file: global.json
- source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore
@@ -45,6 +50,10 @@ jobs:
- name: Test
run: dotnet test -c Release --no-build --logger GitHubActions
+ - name: aot-publish test
+ run: |
+ dotnet publish ./samples/AspNetCore/Samples.AspNetCore.csproj
+
packaging:
needs: build
@@ -58,18 +67,23 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
submodules: recursive
- name: Setup .NET SDK
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
with:
global-json-file: global.json
- source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore
@@ -88,7 +102,7 @@ jobs:
- name: Publish NuGet packages (fork)
if: github.event.pull_request.head.repo.fork == true
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: nupkgs
path: src/**/*.nupkg
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index fc7c37f5c..c221b5911 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -22,22 +22,27 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
- name: Setup .NET SDK
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
with:
global-json-file: global.json
- source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
- name: Run Test
run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
- - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
+ - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
name: Code Coverage for ${{ matrix.os }}
fail_ci_if_error: true
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index c1785d201..af80cac1d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@39edc492dbe16b1465b0cafca41432d857bdb31a # v3
+ uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@39edc492dbe16b1465b0cafca41432d857bdb31a # v3
+ uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
# โน๏ธ Command-line programs to run using the OS shell.
# ๐ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@39edc492dbe16b1465b0cafca41432d857bdb31a # v3
+ uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
index e35e37756..e4326e045 100644
--- a/.github/workflows/dotnet-format.yml
+++ b/.github/workflows/dotnet-format.yml
@@ -15,12 +15,14 @@ jobs:
steps:
- name: Check out code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Setup .NET SDK
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
with:
global-json-file: global.json
- name: dotnet format
- run: dotnet format --verify-no-changes OpenFeature.slnx
+ run: |
+ # Exclude diagnostics to work around dotnet-format issue, see https://github.com/dotnet/sdk/issues/50012
+ dotnet format --verify-no-changes OpenFeature.slnx --exclude-diagnostics IL2026 --exclude-diagnostics IL3050
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index ae0ca8391..fa02626eb 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -17,17 +17,22 @@ jobs:
contents: read
pull-requests: write
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
- name: Setup .NET SDK
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
with:
global-json-file: global.json
- source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
- name: Initialize Tests
run: |
diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml
index f23079276..2aad982e7 100644
--- a/.github/workflows/lint-pr.yml
+++ b/.github/workflows/lint-pr.yml
@@ -12,9 +12,31 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
- contents: read
pull-requests: write
steps:
- - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5
+ - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6
+ id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
+ # When the previous steps fails, the workflow would stop. By adding this
+ # condition you can continue the execution with the populated error message.
+ if: always() && (steps.lint_pr_title.outputs.error_message != null)
+ with:
+ header: pr-title-lint-error
+ message: |
+ Hey there and thank you for opening this pull request! ๐๐ผ
+
+ We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
+ Details:
+
+ ```
+ ${{ steps.lint_pr_title.outputs.error_message }}
+ ```
+ # Delete a previous comment when the issue has been resolved
+ - if: ${{ steps.lint_pr_title.outputs.error_message == null }}
+ uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
+ with:
+ header: pr-title-lint-error
+ delete: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 725957a75..4468cb37d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 #v4
+ - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4
id: release
with:
token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}}
@@ -32,24 +32,29 @@ jobs:
runs-on: ubuntu-latest
needs: release-please
permissions:
- id-token: write
+ id-token: write # enable GitHub OIDC token issuance for this job (NuGet login)
contents: write # for SBOM release
attestations: write # for actions/attest-sbom to create attestation
packages: read # for internal nuget reading
if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
- name: Setup .NET SDK
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5
with:
global-json-file: global.json
- source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
- name: Install dependencies
run: dotnet restore
@@ -57,11 +62,18 @@ jobs:
- name: Pack
run: dotnet pack -c Release --no-restore
+ # Get a short-lived NuGet API key
+ - name: NuGet login (OIDC โ temp API key)
+ uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1
+ id: login
+ with:
+ user: ${{secrets.NUGET_USER}}
+
- name: Publish to Nuget
- run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json
+ run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json
- name: Generate artifact attestation
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
+ uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: "src/**/*.nupkg"
@@ -81,10 +93,10 @@ jobs:
project-name: OpenFeature.Hosting
release-tag: ${{ needs.release-please.outputs.release_tag_name }}
- # Process OpenFeature.DependencyInjection project
- - name: Generate and Attest SBOM for OpenFeature.DependencyInjection
+ # Process OpenFeature.Providers.MultiProvider project
+ - name: Generate and Attest SBOM for OpenFeature.Providers.MultiProvider
uses: ./.github/actions/sbom-generator
with:
github-token: ${{secrets.GITHUB_TOKEN}}
- project-name: OpenFeature.DependencyInjection
+ project-name: OpenFeature.Providers.MultiProvider
release-tag: ${{ needs.release-please.outputs.release_tag_name }}
diff --git a/.gitignore b/.gitignore
index 055ffe50f..5648ca1dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,6 @@
*.user
*.userosscache
*.sln.docstates
-.devcontainer/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -352,5 +351,7 @@ ASALocalRun/
# integration tests
test/OpenFeature.E2ETests/Features/*.feature
test/OpenFeature.E2ETests/Features/*.feature.cs
+test/OpenFeature.E2ETests/Features/README.md
+test/OpenFeature.E2ETests/Features/test-flags.json
cs-report.json
specification.json
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6ed9c8012..a9b8e02a9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "2.7.0"
+ ".": "2.11.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a176c613..3c7b76af9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,105 @@
# Changelog
+## [2.11.0](https://github.com/open-feature/dotnet-sdk/compare/v2.10.0...v2.11.0) (2025-12-18)
+
+
+### โจ New Features
+
+* Upgrade to dotnet version 10 ([#658](https://github.com/open-feature/dotnet-sdk/issues/658)) ([c7be7e0](https://github.com/open-feature/dotnet-sdk/commit/c7be7e0fbff694ec9ef794548f6e7a478412b68b))
+
+## [2.10.0](https://github.com/open-feature/dotnet-sdk/compare/v2.9.0...v2.10.0) (2025-12-01)
+
+
+### ๐ Bug Fixes
+
+* Address issue with FeatureClient not being resolved when no Provider added ([#607](https://github.com/open-feature/dotnet-sdk/issues/607)) ([a8d12ef](https://github.com/open-feature/dotnet-sdk/commit/a8d12ef12d75aaa770551b3052cd8725b65b5fd8))
+* Address issues when evaluating the context in the InMemoryProvider ([#615](https://github.com/open-feature/dotnet-sdk/issues/615)) ([94fcdc1](https://github.com/open-feature/dotnet-sdk/commit/94fcdc142c61f41619af222778d6d84264f2831c))
+* Ensure AddPolicyName without adding a Provider does not get stuck in infinite loop ([#606](https://github.com/open-feature/dotnet-sdk/issues/606)) ([4b965dd](https://github.com/open-feature/dotnet-sdk/commit/4b965dddcaeef761e01f8fcbd28941ae3f3074c9))
+* Ensure EvaluationContext is reliably added to the injected FeatureClient ([#605](https://github.com/open-feature/dotnet-sdk/issues/605)) ([c987b58](https://github.com/open-feature/dotnet-sdk/commit/c987b58b66c8186486fd06aebdc4042052f30beb))
+
+
+### โจ New Features
+
+* Add DI for multi provider ([#621](https://github.com/open-feature/dotnet-sdk/issues/621)) ([ee862f0](https://github.com/open-feature/dotnet-sdk/commit/ee862f09cb2c58f43f84957fa95e8b25e8e36f72))
+* Add disabled flag support to InMemoryProvider ([#632](https://github.com/open-feature/dotnet-sdk/issues/632)) ([df1765c](https://github.com/open-feature/dotnet-sdk/commit/df1765c7abc4e9e5f76954ddb361b3fd5bf0ddf7))
+* Add optional CancellationToken parameter to SetProviderAsync ([#638](https://github.com/open-feature/dotnet-sdk/issues/638)) ([a1f7ff6](https://github.com/open-feature/dotnet-sdk/commit/a1f7ff6434842ff051e32af5c787e1bf40a5cb66))
+* Add SourceLink configuration for .NET SDK 8+ to enhance debugging experience ([1b40391](https://github.com/open-feature/dotnet-sdk/commit/1b40391034b0762aa755a05374a908eb97cdf444))
+* Add SourceLink configuration for .NET to enhance debugging experience ([#614](https://github.com/open-feature/dotnet-sdk/issues/614)) ([1b40391](https://github.com/open-feature/dotnet-sdk/commit/1b40391034b0762aa755a05374a908eb97cdf444))
+* Add tracking to multi-provider ([#612](https://github.com/open-feature/dotnet-sdk/issues/612)) ([186b357](https://github.com/open-feature/dotnet-sdk/commit/186b3574702258fb33716162094888b9f7560c7c))
+
+
+### ๐ง Refactoring
+
+* Clean up project files by removing TargetFrameworks and formatting ([#611](https://github.com/open-feature/dotnet-sdk/issues/611)) ([dfbc3ee](https://github.com/open-feature/dotnet-sdk/commit/dfbc3eef1f7468dc363c71fef1eb1f42e1bb8a88))
+* Pass cancellation tokens to Provider Initialization functions ([#640](https://github.com/open-feature/dotnet-sdk/issues/640)) ([8b472d8](https://github.com/open-feature/dotnet-sdk/commit/8b472d8ccd1367ba82a2ab39ad7a77b1a6609ce0))
+* Remove deprecated Dependency Injection code ([#626](https://github.com/open-feature/dotnet-sdk/issues/626)) ([a36a906](https://github.com/open-feature/dotnet-sdk/commit/a36a9067102a70f80e7837ce18d287430c7452fc))
+
+## [2.9.0](https://github.com/open-feature/dotnet-sdk/compare/v2.8.1...v2.9.0) (2025-10-16)
+
+
+### ๐ Bug Fixes
+
+* update provider status to Fatal during disposal ([#580](https://github.com/open-feature/dotnet-sdk/issues/580)) ([76bd94b](https://github.com/open-feature/dotnet-sdk/commit/76bd94b03ea19ad3c432a52dd644317e362b99ec))
+
+
+### โจ New Features
+
+* Add events to the multi provider ([#568](https://github.com/open-feature/dotnet-sdk/issues/568)) ([9d8ab03](https://github.com/open-feature/dotnet-sdk/commit/9d8ab037df1749d098f5e1e210f71cf9d1e7adff))
+* Add multi-provider support ([#488](https://github.com/open-feature/dotnet-sdk/issues/488)) ([7237053](https://github.com/open-feature/dotnet-sdk/commit/7237053561d9c36194197169734522f0b978f6e5))
+* Deprecate AddHostedFeatureLifecycle method ([#531](https://github.com/open-feature/dotnet-sdk/issues/531)) ([fdf2297](https://github.com/open-feature/dotnet-sdk/commit/fdf229737118639d323e74cceac490d44c4c24dd))
+* Implement hooks in multi provider ([#594](https://github.com/open-feature/dotnet-sdk/issues/594)) ([95ae7f0](https://github.com/open-feature/dotnet-sdk/commit/95ae7f03249e351c20ccd6152d88400a7e1ef764))
+* Support retrieving numeric metadata as either integers or decimals ([#490](https://github.com/open-feature/dotnet-sdk/issues/490)) ([12de5f1](https://github.com/open-feature/dotnet-sdk/commit/12de5f10421bac749fdd45c748e7b970f3f69a39))
+
+
+### ๐ Performance
+
+* Add NativeAOT Support ([#554](https://github.com/open-feature/dotnet-sdk/issues/554)) ([acd0486](https://github.com/open-feature/dotnet-sdk/commit/acd0486563f7b67a782ee169315922fb5d0f343e))
+
+## [2.8.1](https://github.com/open-feature/dotnet-sdk/compare/v2.8.0...v2.8.1) (2025-07-31)
+
+
+### ๐ Bug Fixes
+
+* expose ValueJsonConverter for generator support and add JsonSourceGenerator test cases ([#537](https://github.com/open-feature/dotnet-sdk/issues/537)) ([e03aeba](https://github.com/open-feature/dotnet-sdk/commit/e03aeba0f515f668afaba0a3c6f0ea01b44d6ee4))
+
+## [2.8.0](https://github.com/open-feature/dotnet-sdk/compare/v2.7.0...v2.8.0) (2025-07-30)
+
+
+### ๐ Bug Fixes
+
+* update DI lifecycle to use container instead of static instance ([#534](https://github.com/open-feature/dotnet-sdk/issues/534)) ([1a3846d](https://github.com/open-feature/dotnet-sdk/commit/1a3846d7575e75b5d7d05ec2a7db0b0f82c7b274))
+
+
+### โจ New Features
+
+* Add Hook Dependency Injection extension method with Hook instance ([#513](https://github.com/open-feature/dotnet-sdk/issues/513)) ([12396b7](https://github.com/open-feature/dotnet-sdk/commit/12396b7872a2db6533b33267cf9c299248c41472))
+* Add TraceEnricherHookOptions Custom Attributes ([#526](https://github.com/open-feature/dotnet-sdk/issues/526)) ([5a91005](https://github.com/open-feature/dotnet-sdk/commit/5a91005c888c8966145eae7745cc40b2b066f343))
+* Add Track method to IFeatureClient ([#519](https://github.com/open-feature/dotnet-sdk/issues/519)) ([2e70072](https://github.com/open-feature/dotnet-sdk/commit/2e7007277e19a0fbc4c4c3944d24eea1608712e6))
+* Support JSON Serialize for Value ([#529](https://github.com/open-feature/dotnet-sdk/issues/529)) ([6e521d2](https://github.com/open-feature/dotnet-sdk/commit/6e521d25c3dd53c45f2fd30f5319cae5cd2ff46d))
+* Add Metric Hook Custom Attributes ([#512](https://github.com/open-feature/dotnet-sdk/issues/512)) ([8c05d1d](https://github.com/open-feature/dotnet-sdk/commit/8c05d1d7363db89b8379e1a4e46e455f210888e2))
+
+
+### ๐งน Chore
+
+* Add comparison to Value ([#523](https://github.com/open-feature/dotnet-sdk/issues/523)) ([883f4f3](https://github.com/open-feature/dotnet-sdk/commit/883f4f3c8b553dc01b5accdbae2782ca7805e8ed))
+* **deps:** update github/codeql-action digest to 181d5ee ([#520](https://github.com/open-feature/dotnet-sdk/issues/520)) ([40bec0d](https://github.com/open-feature/dotnet-sdk/commit/40bec0d51b6fa782a8b6d90a3d84463f9fb73c1b))
+* **deps:** update github/codeql-action digest to 4e828ff ([#532](https://github.com/open-feature/dotnet-sdk/issues/532)) ([20d1f37](https://github.com/open-feature/dotnet-sdk/commit/20d1f37a4f8991419bb14dae7eec9a08c2b32bc6))
+* **deps:** update github/codeql-action digest to d6bbdef ([#527](https://github.com/open-feature/dotnet-sdk/issues/527)) ([03d3b9e](https://github.com/open-feature/dotnet-sdk/commit/03d3b9e5d6ff1706faffc25afeba80a0e2bb37ec))
+* **deps:** update spec digest to 224b26e ([#521](https://github.com/open-feature/dotnet-sdk/issues/521)) ([fbc2645](https://github.com/open-feature/dotnet-sdk/commit/fbc2645efd649c0c37bd1a1cf473fbd98d920948))
+* **deps:** update spec digest to baec39b ([#528](https://github.com/open-feature/dotnet-sdk/issues/528)) ([a0ae014](https://github.com/open-feature/dotnet-sdk/commit/a0ae014d3194fcf6e5e5e4a17a2f92b1df3dc7c7))
+* remove redundant rule (now in parent) ([929fa74](https://github.com/open-feature/dotnet-sdk/commit/929fa7497197214d385eeaa40aba008932d00896))
+
+
+### ๐ Documentation
+
+* fix anchor link in readme ([#525](https://github.com/open-feature/dotnet-sdk/issues/525)) ([18705c7](https://github.com/open-feature/dotnet-sdk/commit/18705c7338a0c89f163f808c81e513a029c95239))
+* remove curly brace from readme ([8c92524](https://github.com/open-feature/dotnet-sdk/commit/8c92524edbf4579d4ad62c699b338b9811a783fd))
+
+
+### ๐ Refactoring
+
+* Simplify Provider Repository ([#515](https://github.com/open-feature/dotnet-sdk/issues/515)) ([2547a57](https://github.com/open-feature/dotnet-sdk/commit/2547a574e0d0328f909b7e69f3775d07492de3dd))
+
## [2.7.0](https://github.com/open-feature/dotnet-sdk/compare/v2.6.0...v2.7.0) (2025-07-03)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 98800faf8..e3c6300b7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,6 +18,26 @@ On all platforms, the minimum requirements are:
- JetBrains Rider 2022.2+ or Visual Studio 2022+ or Visual Studio Code
- .NET Framework 4.6.2+
+### Development Containers
+
+This repository includes support for [Development Containers](https://containers.dev/) (devcontainers), which provide a consistent, containerized development environment. The devcontainer configuration includes all necessary dependencies and tools pre-configured.
+
+To use the devcontainer:
+
+1. **Prerequisites**: Install [Docker](https://www.docker.com/) and either:
+ - [Visual Studio Code](https://code.visualstudio.com/) with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
+ - [GitHub Codespaces](https://github.com/features/codespaces)
+
+2. **Using with VS Code**:
+ - Open the repository in VS Code
+ - When prompted, click "Reopen in Container" or use the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and select "Dev Containers: Reopen in Container"
+
+3. **Using with GitHub Codespaces**:
+ - Navigate to the repository on GitHub
+ - Click the "Code" button and select "Create codespace on [branch-name]"
+
+The devcontainer provides a pre-configured environment with the .NET SDK and all necessary tools for development and testing.
+
## Pull Request
All contributions to the OpenFeature project are welcome via GitHub pull requests.
@@ -130,6 +150,16 @@ Please make sure you follow the latest [conventions](https://www.conventionalcom
If you want to point out a breaking change, you should use `!` after the type. For example: `feat!: excellent new feature`.
+### Changelog Visibility and Release Triggers
+
+Only certain types are visible in the generated changelog:
+
+- `feat`: โจ New Features - New functionality added
+- `fix`: ๐ Bug Fixes - Bug fixes and corrections
+- `perf`: ๐ Performance - Performance improvements
+- `refactor`: ๐ง Refactoring - Code changes that neither fix bugs nor add features
+- `revert`: ๐ Reverts - Reverted changes
+
## Design Choices
As with other OpenFeature SDKs, dotnet-sdk follows the
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 041434a47..622dcc537 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,35 +7,57 @@
9.0.0
+
+ 10.0.0
+
-
-
-
-
+
+
+
+
+
-
+
+
-
+
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
index 261eeb9e9..96b3dc8fc 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright OpenFeature Maintainers
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index d6778e50e..db8f40024 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -5,7 +5,7 @@
-
+
@@ -14,9 +14,6 @@
-
-
-
@@ -51,18 +48,20 @@
-
-
-
+
+
+
-
+
-
+
+
+
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 66a3d620c..dfedfabe1 100644
--- a/README.md
+++ b/README.md
@@ -10,8 +10,8 @@
[](https://github.com/open-feature/spec/releases/tag/v0.8.0)
[
-
-](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.7.0)
+
+](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.11.0)
[](https://cloud-native.slack.com/archives/C0344AANLA1)
[](https://codecov.io/gh/open-feature/dotnet-sdk)
@@ -33,6 +33,12 @@
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
+### NativeAOT Support
+
+โ **Full NativeAOT Compatibility** - The OpenFeature .NET SDK is fully compatible with .NET NativeAOT compilation for fast startup and small deployment size. See the [AOT Compatibility Guide](docs/AOT_COMPATIBILITY.md) for detailed instructions.
+
+> While the core OpenFeature SDK is fully NativeAOT compatible, contrib and community-provided providers, hooks, and extensions may not be. Please check with individual provider/hook documentation for their NativeAOT compatibility status.
+
### Install
Use the following to initialize your project:
@@ -113,7 +119,8 @@ Want to contribute a new sample? See our [CONTRIBUTING](CONTRIBUTING.md) guide!
| โ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| โ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
| โ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
-| ๐ฌ | [DependencyInjection](#DependencyInjection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |
+| ๐ฌ | [Multi-Provider](#multi-provider) | Use multiple feature flag providers simultaneously with configurable evaluation strategies. |
+| ๐ฌ | [DependencyInjection](#dependency-injection) | Integrate OpenFeature with .NET's dependency injection for streamlined provider setup. |
> Implemented: โ | In-progress: โ ๏ธ | Not implemented yet: โ | Experimental: ๐ฌ
@@ -433,17 +440,106 @@ Hooks support passing per-evaluation data between that stages using `hook data`.
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
+### Multi-Provider
> [!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.
+> The Multi-Provider feature is currently experimental.
+
+The Multi-Provider enables the use of multiple underlying feature flag providers simultaneously, allowing different providers to be used for different flag keys or based on specific evaluation strategies.
+
+The Multi-Provider supports provider hooks and executes them in accordance with the OpenFeature specification. Each provider's hooks are executed with context isolation, ensuring that context modifications by one provider's hooks do not affect other providers.
+
+#### Basic Usage
+
+```csharp
+using OpenFeature.Providers.MultiProvider;
+using OpenFeature.Providers.MultiProvider.Models;
+using OpenFeature.Providers.MultiProvider.Strategies;
+
+// Create provider entries
+var providerEntries = new List
+{
+ new(new InMemoryProvider(provider1Flags), "Provider1"),
+ new(new InMemoryProvider(provider2Flags), "Provider2")
+};
+
+// Create multi-provider with FirstMatchStrategy (default)
+var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
+
+// Set as the default provider
+await Api.Instance.SetProviderAsync(multiProvider);
+
+// Use normally - the multi-provider will handle delegation
+var client = Api.Instance.GetClient();
+var flagValue = await client.GetBooleanValueAsync("my-flag", false);
+```
+
+#### Evaluation Strategies
+
+The Multi-Provider supports different evaluation strategies that determine how multiple providers are used:
+
+##### FirstMatchStrategy (Default)
+
+Evaluates providers sequentially and returns the first result that is not "flag not found". If any provider returns an error, that error is returned immediately.
+
+```csharp
+var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
+```
+
+##### FirstSuccessfulStrategy
+
+Evaluates providers sequentially and returns the first successful result, ignoring errors. Only if all providers fail will errors be returned.
+
+```csharp
+var multiProvider = new MultiProvider(providerEntries, new FirstSuccessfulStrategy());
+```
+
+##### ComparisonStrategy
+
+Evaluates all providers in parallel and compares results. If values agree, returns the agreed value. If they disagree, returns the fallback provider's value (or first provider if no fallback is specified) and optionally calls a mismatch callback.
+
+```csharp
+// Basic comparison
+var multiProvider = new MultiProvider(providerEntries, new ComparisonStrategy());
+
+// With fallback provider
+var multiProvider = new MultiProvider(providerEntries,
+ new ComparisonStrategy(fallbackProvider: provider1));
+
+// With mismatch callback
+var multiProvider = new MultiProvider(providerEntries,
+ new ComparisonStrategy(onMismatch: (mismatchDetails) => {
+ // Log or handle mismatches between providers
+ foreach (var kvp in mismatchDetails)
+ {
+ Console.WriteLine($"Provider {kvp.Key}: {kvp.Value}");
+ }
+ }));
+```
+
+#### Evaluation Modes
+
+The Multi-Provider supports two evaluation modes:
+
+- **Sequential**: Providers are evaluated one after another (used by `FirstMatchStrategy` and `FirstSuccessfulStrategy`)
+- **Parallel**: All providers are evaluated simultaneously (used by `ComparisonStrategy`)
+
+#### Limitations
+
+- **Experimental status**: The API may change in future releases
+
+For a complete example, see the [AspNetCore sample](./samples/AspNetCore/README.md) which demonstrates Multi-Provider usage.
+
+### Dependency Injection
+
+> [!NOTE]
+> The OpenFeature.Hosting package is currently experimental. The Hosting package streamlines 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:
+To set up dependency injection and hosting capabilities for OpenFeature, install the following package:
```sh
-dotnet add package OpenFeature.DependencyInjection
dotnet add package OpenFeature.Hosting
```
@@ -456,7 +552,6 @@ For a basic configuration, you can use the InMemoryProvider. This provider is si
```csharp
builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
- .AddHostedFeatureLifecycle() // From Hosting package
.AddInMemoryProvider();
});
```
@@ -478,9 +573,9 @@ builder.Services.AddOpenFeature(featureBuilder => {
```csharp
builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
- .AddHostedFeatureLifecycle()
.AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ })
.AddHook((serviceProvider) => new LoggingHook( /* Custom configuration */ ))
+ .AddHook(new MetricsHook())
.AddInMemoryProvider("name1")
.AddInMemoryProvider("name2")
.AddPolicyName(options => {
@@ -603,6 +698,24 @@ namespace OpenFeatureTestApp
After running this example, you will be able to see the traces, including the events sent by the hook in your Jaeger UI.
+You can specify custom tags on spans created by the `TraceEnricherHook` by providing `TraceEnricherHookOptions` when adding the hook:
+
+```csharp
+var options = TraceEnricherHookOptions.CreateBuilder()
+ .WithTag("custom_dimension_key", "custom_dimension_value")
+ .Build();
+
+OpenFeature.Api.Instance.AddHooks(new TraceEnricherHook(options));
+```
+
+You can also write your own extraction logic against the Flag metadata by providing a callback to `WithFlagEvaluationMetadata`. The below example will add a tag to the span with the key `boolean` and a value specified by the callback.
+
+```csharp
+var options = TraceEnricherHookOptions.CreateBuilder()
+ .WithFlagEvaluationMetadata("boolean", s => s.GetBool("boolean"))
+ .Build();
+```
+
### Metrics Hook
For this hook to function correctly a global `MeterProvider` must be set.
@@ -610,12 +723,12 @@ For this hook to function correctly a global `MeterProvider` must be set.
Below are the metrics extracted by this hook and dimensions they carry:
-| Metric key | Description | Unit | Dimensions |
-| -------------------------------------- | ------------------------------- | ------------ | ----------------------------- |
-| feature_flag.evaluation_requests_total | Number of evaluation requests | {request} | key, provider name |
-| feature_flag.evaluation_success_total | Flag evaluation successes | {impression} | key, provider name, reason |
-| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
-| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
+| Metric key | Description | Unit | Dimensions |
+| -------------------------------------- | ------------------------------- | ---------- | ----------------------------- |
+| feature_flag.evaluation_requests_total | Number of evaluation requests | request | key, provider name |
+| feature_flag.evaluation_success_total | Flag evaluation successes | impression | key, provider name, reason |
+| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
+| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
Consider the following code example for usage.
@@ -663,6 +776,24 @@ namespace OpenFeatureTestApp
After running this example, you should be able to see some metrics being generated into the console.
+You can specify custom dimensions on all instruments by the `MetricsHook` by providing `MetricsHookOptions` when adding the hook:
+
+```csharp
+var options = MetricsHookOptions.CreateBuilder()
+ .WithCustomDimension("custom_dimension_key", "custom_dimension_value")
+ .Build();
+
+OpenFeature.Api.Instance.AddHooks(new MetricsHook(options));
+```
+
+You can also write your own extraction logic against the Flag metadata by providing a callback to `WithFlagEvaluationMetadata`.
+
+```csharp
+var options = MetricsHookOptions.CreateBuilder()
+ .WithFlagEvaluationMetadata("boolean", s => s.GetBool("boolean"))
+ .Build();
+```
+
## โญ๏ธ Support the project
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 0a05126f5..cfcd00340 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -1,5 +1,5 @@
-
+ true
@@ -9,7 +9,7 @@
- 2.7.0
+ 2.11.0githttps://github.com/open-feature/dotnet-sdkOpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings.
@@ -24,8 +24,25 @@
$(VersionNumber)
+
+
+ true
+
+
-
+
+
+
+
+
+ true
+
+ true
+
+ true
+ snupkg
+
+
diff --git a/build/Common.props b/build/Common.props
index 287b32312..41df868d0 100644
--- a/build/Common.props
+++ b/build/Common.props
@@ -28,4 +28,11 @@
+
+
+
+ true
+
diff --git a/build/Common.samples.props b/build/Common.samples.props
index a5b06c9b9..6b2058a0e 100644
--- a/build/Common.samples.props
+++ b/build/Common.samples.props
@@ -1,6 +1,6 @@
- net9.0
+ net10.0enableenabletrue
diff --git a/docs/AOT_COMPATIBILITY.md b/docs/AOT_COMPATIBILITY.md
new file mode 100644
index 000000000..d4b72952b
--- /dev/null
+++ b/docs/AOT_COMPATIBILITY.md
@@ -0,0 +1,152 @@
+# OpenFeature .NET SDK - NativeAOT Compatibility
+
+The OpenFeature .NET SDK is compatible with .NET NativeAOT compilation, allowing you to create self-contained, native executables with faster startup times and lower memory usage.
+
+## Compatibility Status
+
+**Fully Compatible** - The SDK can be used in NativeAOT applications without any issues.
+
+### What's AOT-Compatible
+
+- Core API functionality (`Api.Instance`, `GetClient()`, flag evaluations)
+- All built-in providers (`NoOpProvider`, etc.)
+- JSON serialization of `Value`, `Structure`, and `EvaluationContext`
+- Error handling and enum descriptions
+- Hook system
+- Event handling
+- Metrics collection
+- Dependency injection
+
+## Using OpenFeature with NativeAOT
+
+### 1. Project Configuration
+
+To enable NativeAOT in your project, add these properties to your `.csproj` file:
+
+```xml
+
+
+ net8.0
+ Exe
+
+
+ true
+
+
+
+
+
+
+```
+
+### 2. Basic Usage
+
+```csharp
+using OpenFeature;
+using OpenFeature.Model;
+
+// Basic OpenFeature usage - fully AOT compatible
+var api = Api.Instance;
+var client = api.GetClient("my-app");
+
+// All flag evaluation methods work
+var boolFlag = await client.GetBooleanValueAsync("feature-enabled", false);
+var stringFlag = await client.GetStringValueAsync("welcome-message", "Hello");
+var intFlag = await client.GetIntegerValueAsync("max-items", 10);
+```
+
+### 3. JSON Serialization (Recommended)
+
+For optimal AOT performance, use the provided `JsonSerializerContext`:
+
+```csharp
+using System.Text.Json;
+using OpenFeature.Model;
+using OpenFeature.Serialization;
+
+var value = new Value(Structure.Builder()
+ .Set("name", "test")
+ .Set("enabled", true)
+ .Build());
+
+// Use AOT-compatible serialization
+var json = JsonSerializer.Serialize(value, OpenFeatureJsonSerializerContext.Default.Value);
+var deserialized = JsonSerializer.Deserialize(json, OpenFeatureJsonSerializerContext.Default.Value);
+```
+
+### 4. Publishing for NativeAOT
+
+Build and publish your AOT application:
+
+```bash
+# Build with AOT analysis
+dotnet build -c Release
+
+# Publish as native executable
+dotnet publish -c Release
+
+# Run the native executable (example path for macOS ARM64)
+./bin/Release/net9.0/osx-arm64/publish/MyApp
+```
+
+## Performance Benefits
+
+NativeAOT compilation provides several benefits:
+
+- **Faster Startup**: Native executables start faster than JIT-compiled applications
+- **Lower Memory Usage**: Reduced memory footprint
+- **Self-Contained**: No .NET runtime dependency required
+- **Smaller Deployment**: Optimized for size with trimming
+
+## Testing AOT Compatibility
+
+The SDK includes an AOT compatibility test project at `test/OpenFeature.AotCompatibility/` that:
+
+- Tests all core SDK functionality
+- Validates JSON serialization with source generation
+- Verifies error handling works correctly
+- Can be compiled and run as a native executable
+
+Run the test:
+
+```bash
+cd test/OpenFeature.AotCompatibility
+dotnet publish -c Release
+./bin/Release/net9.0/[runtime]/publish/OpenFeature.AotCompatibility
+```
+
+## Limitations
+
+Currently, there are no known limitations when using OpenFeature with NativeAOT. All core functionality is fully supported.
+
+## Provider Compatibility
+
+When using third-party providers, ensure they are also AOT-compatible. Check the provider's documentation for AOT support.
+
+## Troubleshooting
+
+### Trimming Warnings
+
+If you encounter trimming warnings, you can:
+
+1. Use the provided `JsonSerializerContext` for JSON operations
+2. Ensure your providers are AOT-compatible
+3. Add appropriate `[DynamicallyAccessedMembers]` attributes if needed
+
+### Build Issues
+
+- Ensure you're targeting .NET 8.0 or later
+- Verify all dependencies support NativeAOT
+- Check that `PublishAot` is set to `true`
+
+## Migration Guide
+
+If migrating from a non-AOT setup:
+
+1. **JSON Serialization**: Replace direct `JsonSerializer` calls with the provided context
+2. **Reflection**: The SDK no longer uses reflection, but ensure your custom code doesn't
+3. **Dynamic Loading**: Avoid dynamic assembly loading; register providers at compile time
+
+## Example AOT Application
+
+See the complete example in `test/OpenFeature.AotCompatibility/Program.cs` for a working AOT application that demonstrates all SDK features.
diff --git a/global.json b/global.json
index 5fb240dd3..d8d11dbec 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
"rollForward": "latestFeature",
- "version": "9.0.300",
+ "version": "10.0.100",
"allowPrerelease": false
}
}
diff --git a/nuget.config b/nuget.config
deleted file mode 100644
index 5a0edf435..000000000
--- a/nuget.config
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/release-please-config.json b/release-please-config.json
index 5a0201f6d..1f778ed73 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -23,10 +23,12 @@
},
{
"type": "chore",
+ "hidden": true,
"section": "๐งน Chore"
},
{
"type": "docs",
+ "hidden": true,
"section": "๐ Documentation"
},
{
@@ -40,6 +42,7 @@
},
{
"type": "deps",
+ "hidden": true,
"section": "๐ฆ Dependencies"
},
{
@@ -49,7 +52,7 @@
},
{
"type": "refactor",
- "section": "๐ Refactoring"
+ "section": "๐ง Refactoring"
},
{
"type": "revert",
diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs
index 5f4f01461..87238c158 100644
--- a/samples/AspNetCore/Program.cs
+++ b/samples/AspNetCore/Program.cs
@@ -1,16 +1,27 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using OpenFeature;
-using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
+using OpenFeature.Hosting.Providers.Memory;
+using OpenFeature.Model;
using OpenFeature.Providers.Memory;
-using OpenTelemetry.Logs;
+using OpenFeature.Providers.MultiProvider;
+using OpenFeature.Providers.MultiProvider.DependencyInjection;
+using OpenFeature.Providers.MultiProvider.Models;
+using OpenFeature.Providers.MultiProvider.Strategies;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
-var builder = WebApplication.CreateBuilder(args);
+var builder = WebApplication.CreateSlimBuilder(args);
// Add services to the container.
+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+});
+
builder.Services.AddProblemDetails();
// Configure OpenTelemetry
@@ -26,17 +37,56 @@
builder.Services.AddOpenFeature(featureBuilder =>
{
- featureBuilder.AddHostedFeatureLifecycle()
+ var metricsHookOptions = MetricsHookOptions.CreateBuilder()
+ .WithCustomDimension("custom_dimension_key", "custom_dimension_value")
+ .WithFlagEvaluationMetadata("boolean", s => s.GetBool("boolean"))
+ .Build();
+
+ featureBuilder
.AddHook(sp => new LoggingHook(sp.GetRequiredService>()))
- .AddHook()
+ .AddHook(_ => new MetricsHook(metricsHookOptions))
.AddHook()
.AddInMemoryProvider("InMemory", _ => new Dictionary()
{
{
"welcome-message", new Flag(
new Dictionary { { "show", true }, { "hide", false } }, "show")
+ },
+ {
+ "disabled-flag", new Flag(
+ new Dictionary { { "on", "This flag is on" }, { "off", "This flag is off" } }, "off",
+ disabled: true)
+ },
+ {
+ "test-config", new Flag(new Dictionary()
+ {
+ { "enable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 100).Build()) },
+ { "half", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 50).Build()) },
+ { "disable", new Value(Structure.Builder().Set(nameof(TestConfig.Threshold), 0).Build()) }
+ }, "disable")
}
- });
+ })
+ .AddMultiProvider("multi-provider", multiProviderBuilder =>
+ {
+ // Create provider flags
+ var provider1Flags = new Dictionary
+ {
+ { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider1" }, { "disabled", "disabled-provider1" } }, "enabled") },
+ { "max-items", new Flag(new Dictionary { { "low", 10 }, { "high", 100 } }, "high") },
+ };
+
+ var provider2Flags = new Dictionary
+ {
+ { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") },
+ };
+
+ // Use the factory pattern to create providers - they will be properly initialized
+ multiProviderBuilder
+ .AddProvider("p1", sp => new InMemoryProvider(provider1Flags))
+ .AddProvider("p2", sp => new InMemoryProvider(provider2Flags))
+ .UseStrategy();
+ })
+ .AddPolicyName(policy => policy.DefaultNameSelector = provider => "InMemory");
});
var app = builder.Build();
@@ -56,4 +106,93 @@
return TypedResults.Ok("Hello world!");
});
+app.MapGet("/test-config", async ([FromServices] IFeatureClient featureClient) =>
+{
+ var testConfigValue = await featureClient.GetObjectValueAsync("test-config",
+ new Value(Structure.Builder().Set("Threshold", 50).Build())
+ );
+ var json = JsonSerializer.Serialize(testConfigValue, AppJsonSerializerContext.Default.Value);
+ var config = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.TestConfig);
+ return Results.Ok(config);
+});
+
+app.MapGet("/multi-provider", async () =>
+{
+ // Create first in-memory provider with some flags
+ var provider1Flags = new Dictionary
+ {
+ { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider1" }, { "disabled", "disabled-provider1" } }, "enabled") },
+ { "max-items", new Flag(new Dictionary { { "low", 10 }, { "high", 100 } }, "high") },
+ };
+ var provider1 = new InMemoryProvider(provider1Flags);
+
+ // Create second in-memory provider with different flags
+ var provider2Flags = new Dictionary
+ {
+ { "providername", new Flag(new Dictionary { { "enabled", "enabled-provider2" }, { "disabled", "disabled-provider2" } }, "enabled") },
+ };
+ var provider2 = new InMemoryProvider(provider2Flags);
+
+ // Create provider entries
+ var providerEntries = new List
+ {
+ new(provider1, "Provider1"),
+ new(provider2, "Provider2")
+ };
+
+ // Create multi-provider with FirstMatchStrategy (default)
+ var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
+
+ // Set the multi-provider as the default provider using OpenFeature API
+ await Api.Instance.SetProviderAsync(multiProvider);
+
+ // Create a client directly using the API
+ var client = Api.Instance.GetClient();
+
+ try
+ {
+ // Test flag evaluation from different providers
+ var maxItemsFlag = await client.GetIntegerDetailsAsync("max-items", 0);
+ var providerNameFlag = await client.GetStringDetailsAsync("providername", "default");
+
+ // Test a flag that doesn't exist in any provider
+ var unknownFlag = await client.GetBooleanDetailsAsync("unknown-flag", false);
+
+ return Results.Ok();
+ }
+ catch (Exception)
+ {
+ return Results.InternalServerError();
+ }
+});
+
+app.MapGet("/multi-provider-di", async ([FromKeyedServices("multi-provider")] IFeatureClient featureClient) =>
+{
+ try
+ {
+ // Test flag evaluation from different providers
+ var maxItemsFlag = await featureClient.GetIntegerDetailsAsync("max-items", 0);
+ var providerNameFlag = await featureClient.GetStringDetailsAsync("providername", "default");
+
+ // Test a flag that doesn't exist in any provider
+ var unknownFlag = await featureClient.GetBooleanDetailsAsync("unknown-flag", false);
+
+ return Results.Ok();
+ }
+ catch (Exception ex)
+ {
+ return Results.Problem($"Error: {ex.Message}\n\nStack: {ex.StackTrace}");
+ }
+});
+
app.Run();
+
+
+public class TestConfig
+{
+ public int Threshold { get; set; } = 10;
+}
+
+[JsonSerializable(typeof(TestConfig))]
+[JsonSerializable(typeof(Value))]
+public partial class AppJsonSerializerContext : JsonSerializerContext;
diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj
index cd249ab3e..723bbd4bd 100644
--- a/samples/AspNetCore/Samples.AspNetCore.csproj
+++ b/samples/AspNetCore/Samples.AspNetCore.csproj
@@ -2,18 +2,20 @@
false
+ true
+ true
-
+
-
-
-
+
+
+
diff --git a/spec b/spec
index c37ac17c8..400fa3038 160000
--- a/spec
+++ b/spec
@@ -1 +1 @@
-Subproject commit c37ac17c80410de1a2c6c6f061386001c838cb40
+Subproject commit 400fa3038d4469f0cbf77af3e79fedda406afc21
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 992a61958..bacf8984e 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,3 +1,10 @@
-
+
+
+ net462;netstandard2.0;net8.0;net9.0;net10.0
+
+ $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))
+
diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj
deleted file mode 100644
index 855ab2ab2..000000000
--- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- netstandard2.0;net8.0;net9.0;net462
- OpenFeature.DependencyInjection
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
deleted file mode 100644
index 317589606..000000000
--- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs
+++ /dev/null
@@ -1,338 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Options;
-using OpenFeature.Constant;
-using OpenFeature.DependencyInjection;
-using OpenFeature.DependencyInjection.Internal;
-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 feature provider using a factory method without additional configuration options.
- /// This method adds the feature provider as a transient service and sets it as the default provider within the application.
- ///
- /// The used to configure feature flags.
- ///
- /// A factory method that creates and returns a
- /// instance based on the provided service provider.
- ///
- /// The updated instance with the default feature provider set and configured.
- /// Thrown if the is null, as a valid builder is required to add and configure providers.
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory)
- => AddProvider(builder, implementationFactory, null);
-
- ///
- /// Adds a feature provider using a factory method to create the provider instance and optionally configures its settings.
- /// This method adds the feature provider as a transient service and sets it as the default provider within the application.
- ///
- /// Type derived from used to configure the feature provider.
- /// The used to configure feature flags.
- ///
- /// A factory method that creates and returns a
- /// instance based on the provided service provider.
- ///
- /// An optional delegate to configure the provider-specific options.
- /// The updated instance with the default feature provider set and configured.
- /// Thrown if the is null, as a valid builder is required to add and configure providers.
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory, Action? configureOptions)
- where TOptions : OpenFeatureOptions
- {
- Guard.ThrowIfNull(builder);
-
- builder.HasDefaultProvider = true;
- builder.Services.PostConfigure(options => options.AddDefaultProviderName());
- if (configureOptions != null)
- {
- builder.Services.Configure(configureOptions);
- }
-
- builder.Services.TryAddTransient(implementationFactory);
- builder.AddClient();
- return builder;
- }
-
- ///
- /// Adds a feature provider for a specific domain using provided options and a configuration builder.
- ///
- /// Type derived from used to configure the feature provider.
- /// The used to configure feature flags.
- /// The unique name of the provider.
- ///
- /// A factory method that creates a feature provider instance.
- /// It adds the provider as a transient service unless it is already added.
- ///
- /// An optional delegate to configure the provider-specific options.
- /// The updated instance with the new feature provider configured.
- ///
- /// Thrown if either or is null or if the is empty.
- ///
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory, Action? configureOptions)
- where TOptions : OpenFeatureOptions
- {
- Guard.ThrowIfNull(builder);
-
- builder.DomainBoundProviderRegistrationCount++;
-
- builder.Services.PostConfigure(options => options.AddProviderName(domain));
- if (configureOptions != null)
- {
- builder.Services.Configure(domain, configureOptions);
- }
-
- builder.Services.TryAddKeyedTransient(domain, (provider, key) =>
- {
- if (key == null)
- {
- throw new ArgumentNullException(nameof(key));
- }
- return implementationFactory(provider, key.ToString()!);
- });
-
- builder.AddClient(domain);
- return builder;
- }
-
- ///
- /// Adds a feature provider for a specified domain using the default options.
- /// This method configures a feature provider without custom options, delegating to the more generic AddProvider method.
- ///
- /// The used to configure feature flags.
- /// The unique name of the provider.
- ///
- /// A factory method that creates a feature provider instance.
- /// It adds the provider as a transient service unless it is already added.
- ///
- /// The updated instance with the new feature provider configured.
- ///
- /// Thrown if either or is null or if the is empty.
- ///
- public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory)
- => AddProvider(builder, domain, implementationFactory, configureOptions: null);
-
- ///
- /// 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;
- }
-
- ///
- /// Adds a default to the based on the policy name options.
- /// This method configures the dependency injection container to resolve the appropriate
- /// depending on the policy name selected.
- /// If no name is selected (i.e., null), it retrieves the default client.
- ///
- /// The instance.
- /// The configured instance.
- internal static OpenFeatureBuilder AddPolicyBasedClient(this OpenFeatureBuilder builder)
- {
- builder.Services.AddScoped(provider =>
- {
- var policy = provider.GetRequiredService>().Value;
- var name = policy.DefaultNameSelector(provider);
- if (name == null)
- {
- return provider.GetRequiredService();
- }
- return provider.GetRequiredKeyedService(name);
- });
-
- 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);
-
- ///
- /// Adds a feature hook to the service collection using a factory method. Hooks added here are not domain-bound.
- ///
- /// The type of to be added.
- /// The instance.
- /// Optional factory for controlling how will be created in the DI container.
- /// The instance.
- public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, Func? implementationFactory = null)
- where THook : Hook
- {
- return builder.AddHook(typeof(THook).Name, implementationFactory);
- }
-
- ///
- /// Adds a feature hook to the service collection using a factory method and specified name. Hooks added here are not domain-bound.
- ///
- /// The type of to be added.
- /// The instance.
- /// The name of the that is being added.
- /// Optional factory for controlling how will be created in the DI container.
- /// The instance.
- public static OpenFeatureBuilder AddHook(this OpenFeatureBuilder builder, string hookName, Func? implementationFactory = null)
- where THook : Hook
- {
- builder.Services.PostConfigure(options => options.AddHookName(hookName));
-
- if (implementationFactory is not null)
- {
- builder.Services.TryAddKeyedSingleton(hookName, (serviceProvider, key) =>
- {
- return implementationFactory(serviceProvider);
- });
- }
- else
- {
- builder.Services.TryAddKeyedSingleton(hookName);
- }
-
- return builder;
- }
-
- ///
- /// Add a to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
- ///
- /// The instance.
- /// The type to handle.
- /// The handler which reacts to .
- /// The instance.
- public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, EventHandlerDelegate eventHandlerDelegate)
- {
- return AddHandler(builder, type, _ => eventHandlerDelegate);
- }
-
- ///
- /// Add a to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
- ///
- /// The instance.
- /// The type to handle.
- /// The handler factory for creating a handler which reacts to .
- /// The instance.
- public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, Func implementationFactory)
- {
- builder.Services.AddSingleton((serviceProvider) =>
- {
- var handler = implementationFactory(serviceProvider);
- return new EventHandlerDelegateWrapper(type, handler);
- });
-
- return builder;
- }
-}
diff --git a/src/OpenFeature.DependencyInjection/README.md b/src/OpenFeature.DependencyInjection/README.md
new file mode 100644
index 000000000..ba9a1e898
--- /dev/null
+++ b/src/OpenFeature.DependencyInjection/README.md
@@ -0,0 +1,48 @@
+# OpenFeature.DependencyInjection
+
+> **โ ๏ธ DEPRECATED**: This library is now deprecated. The OpenFeature Dependency Injection library has been moved to the OpenFeature Hosting integration in version 2.9.0.
+
+OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings.
+
+## Migration Guide
+
+If you are using `OpenFeature.DependencyInjection`, you should migrate to the `OpenFeature.Hosting` package. The hosting package provides the same functionality but in one package.
+
+### 1. Update dependencies
+
+Remove this package:
+
+```xml
+
+```
+
+Update or install the latest `OpenFeature.Hosting` package:
+
+```xml
+
+```
+
+### 2. Update your `Program.cs`
+
+Remove the `AddHostedFeatureLifecycle` method call.
+
+#### Before
+
+```csharp
+builder.Services.AddOpenFeature(featureBuilder =>
+{
+ featureBuilder
+ .AddHostedFeatureLifecycle();
+
+ // Omit for code brevity
+});
+```
+
+#### After
+
+```csharp
+builder.Services.AddOpenFeature(featureBuilder =>
+{
+ // Omit for code brevity
+});
+```
diff --git a/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs b/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs
similarity index 96%
rename from src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs
rename to src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs
index 582ab39c9..f7ecf81cb 100644
--- a/src/OpenFeature.DependencyInjection/Diagnostics/FeatureCodes.cs
+++ b/src/OpenFeature.Hosting/Diagnostics/FeatureCodes.cs
@@ -1,4 +1,4 @@
-namespace OpenFeature.DependencyInjection.Diagnostics;
+namespace OpenFeature.Hosting.Diagnostics;
///
/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework.
diff --git a/src/OpenFeature.DependencyInjection/Guard.cs b/src/OpenFeature.Hosting/Guard.cs
similarity index 53%
rename from src/OpenFeature.DependencyInjection/Guard.cs
rename to src/OpenFeature.Hosting/Guard.cs
index 337a8290f..2d37ef54d 100644
--- a/src/OpenFeature.DependencyInjection/Guard.cs
+++ b/src/OpenFeature.Hosting/Guard.cs
@@ -1,7 +1,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
-namespace OpenFeature.DependencyInjection;
+namespace OpenFeature.Hosting;
[DebuggerStepThrough]
internal static class Guard
@@ -11,10 +11,4 @@ public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameo
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.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
index 5209a5257..4411c21bb 100644
--- a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
+++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using OpenFeature.DependencyInjection;
namespace OpenFeature.Hosting;
diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.Hosting/IFeatureLifecycleManager.cs
similarity index 97%
rename from src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
rename to src/OpenFeature.Hosting/IFeatureLifecycleManager.cs
index 4891f2e8b..54f791fbc 100644
--- a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs
+++ b/src/OpenFeature.Hosting/IFeatureLifecycleManager.cs
@@ -1,4 +1,4 @@
-namespace OpenFeature.DependencyInjection;
+namespace OpenFeature.Hosting;
///
/// Defines the contract for managing the lifecycle of a feature api.
diff --git a/src/OpenFeature.DependencyInjection/Internal/EventHandlerDelegateWrapper.cs b/src/OpenFeature.Hosting/Internal/EventHandlerDelegateWrapper.cs
similarity index 78%
rename from src/OpenFeature.DependencyInjection/Internal/EventHandlerDelegateWrapper.cs
rename to src/OpenFeature.Hosting/Internal/EventHandlerDelegateWrapper.cs
index d31b3355c..34e000ce2 100644
--- a/src/OpenFeature.DependencyInjection/Internal/EventHandlerDelegateWrapper.cs
+++ b/src/OpenFeature.Hosting/Internal/EventHandlerDelegateWrapper.cs
@@ -1,7 +1,7 @@
using OpenFeature.Constant;
using OpenFeature.Model;
-namespace OpenFeature.DependencyInjection.Internal;
+namespace OpenFeature.Hosting.Internal;
internal record EventHandlerDelegateWrapper(
ProviderEventTypes ProviderEventType,
diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
similarity index 97%
rename from src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs
rename to src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
index 1ecac4349..4d915946b 100644
--- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs
+++ b/src/OpenFeature.Hosting/Internal/FeatureLifecycleManager.cs
@@ -2,7 +2,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-namespace OpenFeature.DependencyInjection.Internal;
+namespace OpenFeature.Hosting.Internal;
internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager
{
diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.Hosting/MultiTarget/CallerArgumentExpressionAttribute.cs
similarity index 100%
rename from src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs
rename to src/OpenFeature.Hosting/MultiTarget/CallerArgumentExpressionAttribute.cs
diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs b/src/OpenFeature.Hosting/MultiTarget/IsExternalInit.cs
similarity index 100%
rename from src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs
rename to src/OpenFeature.Hosting/MultiTarget/IsExternalInit.cs
diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
index 1d54ff02e..bf570a897 100644
--- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
+++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj
@@ -1,16 +1,22 @@
-
+๏ปฟ
-
- net8.0;net9.0
- OpenFeature
-
+
+ OpenFeature
+ README.md
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.Hosting/OpenFeatureBuilder.cs
similarity index 98%
rename from src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs
rename to src/OpenFeature.Hosting/OpenFeatureBuilder.cs
index ae1e8c8fb..177a9fac3 100644
--- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs
+++ b/src/OpenFeature.Hosting/OpenFeatureBuilder.cs
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
-namespace OpenFeature.DependencyInjection;
+namespace OpenFeature.Hosting;
///
/// Describes a backed by an .
diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
index 80e760d9d..d8b52c6cd 100644
--- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
+++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs
@@ -1,6 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
-using OpenFeature.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenFeature.Constant;
using OpenFeature.Hosting;
+using OpenFeature.Hosting.Internal;
+using OpenFeature.Model;
namespace OpenFeature;
@@ -9,6 +13,367 @@ namespace OpenFeature;
///
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 feature provider using a factory method without additional configuration options.
+ /// This method adds the feature provider as a transient service and sets it as the default provider within the application.
+ ///
+ /// The used to configure feature flags.
+ ///
+ /// A factory method that creates and returns a
+ /// instance based on the provided service provider.
+ ///
+ /// The updated instance with the default feature provider set and configured.
+ /// Thrown if the is null, as a valid builder is required to add and configure providers.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory)
+ => AddProvider(builder, implementationFactory, null);
+
+ ///
+ /// Adds a feature provider using a factory method to create the provider instance and optionally configures its settings.
+ /// This method adds the feature provider as a transient service and sets it as the default provider within the application.
+ ///
+ /// Type derived from used to configure the feature provider.
+ /// The used to configure feature flags.
+ ///
+ /// A factory method that creates and returns a
+ /// instance based on the provided service provider.
+ ///
+ /// An optional delegate to configure the provider-specific options.
+ /// The updated instance with the default feature provider set and configured.
+ /// Thrown if the is null, as a valid builder is required to add and configure providers.
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func implementationFactory, Action? configureOptions)
+ where TOptions : OpenFeatureOptions
+ {
+ Guard.ThrowIfNull(builder);
+
+ builder.HasDefaultProvider = true;
+ builder.Services.PostConfigure(options => options.AddDefaultProviderName());
+ if (configureOptions != null)
+ {
+ builder.Services.Configure(configureOptions);
+ }
+
+ builder.Services.TryAddTransient(implementationFactory);
+ builder.AddClient();
+ return builder;
+ }
+
+ ///
+ /// Adds a feature provider for a specific domain using provided options and a configuration builder.
+ ///
+ /// Type derived from used to configure the feature provider.
+ /// The used to configure feature flags.
+ /// The unique name of the provider.
+ ///
+ /// A factory method that creates a feature provider instance.
+ /// It adds the provider as a transient service unless it is already added.
+ ///
+ /// An optional delegate to configure the provider-specific options.
+ /// The updated instance with the new feature provider configured.
+ ///
+ /// Thrown if either or is null or if the is empty.
+ ///
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory, Action? configureOptions)
+ where TOptions : OpenFeatureOptions
+ {
+ Guard.ThrowIfNull(builder);
+
+ builder.DomainBoundProviderRegistrationCount++;
+
+ builder.Services.PostConfigure(options => options.AddProviderName(domain));
+ if (configureOptions != null)
+ {
+ builder.Services.Configure(domain, configureOptions);
+ }
+
+ builder.Services.TryAddKeyedTransient(domain, (provider, key) =>
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+ return implementationFactory(provider, key.ToString()!);
+ });
+
+ builder.AddClient(domain);
+ return builder;
+ }
+
+ ///
+ /// Adds a feature provider for a specified domain using the default options.
+ /// This method configures a feature provider without custom options, delegating to the more generic AddProvider method.
+ ///
+ /// The used to configure feature flags.
+ /// The unique name of the provider.
+ ///
+ /// A factory method that creates a feature provider instance.
+ /// It adds the provider as a transient service unless it is already added.
+ ///
+ /// The updated instance with the new feature provider configured.
+ ///
+ /// Thrown if either or is null or if the is empty.
+ ///
+ public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Func implementationFactory)
+ => AddProvider(builder, domain, implementationFactory, configureOptions: null);
+
+ ///
+ /// 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))
+ {
+ builder.Services.TryAddScoped(static provider =>
+ {
+ var api = provider.GetRequiredService();
+ var client = api.GetClient();
+
+ var context = provider.GetService();
+ if (context is not null)
+ {
+ client.SetContext(context);
+ }
+
+ return client;
+ });
+ }
+ else
+ {
+ builder.Services.TryAddKeyedScoped(name, static (provider, key) =>
+ {
+ var api = provider.GetRequiredService();
+ var client = api.GetClient(key!.ToString());
+
+ var context = provider.GetService();
+ if (context is not null)
+ {
+ client.SetContext(context);
+ }
+
+ return client;
+ });
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Adds a default to the based on the policy name options.
+ /// This method configures the dependency injection container to resolve the appropriate
+ /// depending on the policy name selected.
+ /// If no name is selected (i.e., null), it retrieves the default client.
+ ///
+ /// The instance.
+ /// The configured instance.
+ internal static OpenFeatureBuilder AddPolicyBasedClient(this OpenFeatureBuilder builder)
+ {
+ builder.Services.AddScoped(provider =>
+ {
+ var policy = provider.GetRequiredService>().Value;
+ var name = policy.DefaultNameSelector(provider);
+ return ResolveFeatureClient(provider, name);
+ });
+
+ return builder;
+ }
+
+ private static IFeatureClient ResolveFeatureClient(IServiceProvider provider, string? name = null)
+ {
+ var api = provider.GetRequiredService();
+ var client = api.GetClient(name);
+ var context = provider.GetService();
+ if (context != null)
+ {
+ client.SetContext(context);
+ }
+
+ return client;
+ }
+
+ ///
+ /// 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);
+
+ ///
+ /// Adds a feature hook to the service collection using a factory method. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// Optional factory for controlling how will be created in the DI container.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>(this OpenFeatureBuilder builder, Func? implementationFactory = null)
+ where THook : Hook
+ {
+ return builder.AddHook(typeof(THook).Name, implementationFactory);
+ }
+
+ ///
+ /// Adds a feature hook to the service collection. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// Instance of Hook to inject into the OpenFeature context.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>(this OpenFeatureBuilder builder, THook hook)
+ where THook : Hook
+ {
+ return builder.AddHook(typeof(THook).Name, hook);
+ }
+
+ ///
+ /// Adds a feature hook to the service collection with a specified name. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// The name of the that is being added.
+ /// Instance of Hook to inject into the OpenFeature context.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>(this OpenFeatureBuilder builder, string hookName, THook hook)
+ where THook : Hook
+ {
+ return builder.AddHook(hookName, _ => hook);
+ }
+
+ ///
+ /// Adds a feature hook to the service collection using a factory method and specified name. Hooks added here are not domain-bound.
+ ///
+ /// The type of to be added.
+ /// The instance.
+ /// The name of the that is being added.
+ /// Optional factory for controlling how will be created in the DI container.
+ /// The instance.
+ public static OpenFeatureBuilder AddHook<
+#if NET
+ [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ THook>
+ (this OpenFeatureBuilder builder, string hookName, Func? implementationFactory = null)
+ where THook : Hook
+ {
+ builder.Services.PostConfigure(options => options.AddHookName(hookName));
+
+ if (implementationFactory is not null)
+ {
+ builder.Services.TryAddKeyedSingleton(hookName, (serviceProvider, key) =>
+ {
+ return implementationFactory(serviceProvider);
+ });
+ }
+ else
+ {
+ builder.Services.TryAddKeyedSingleton(hookName);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Add a to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
+ ///
+ /// The instance.
+ /// The type to handle.
+ /// The handler which reacts to .
+ /// The instance.
+ public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, EventHandlerDelegate eventHandlerDelegate)
+ {
+ return AddHandler(builder, type, _ => eventHandlerDelegate);
+ }
+
+ ///
+ /// Add a to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
+ ///
+ /// The instance.
+ /// The type to handle.
+ /// The handler factory for creating a handler which reacts to .
+ /// The instance.
+ public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, Func implementationFactory)
+ {
+ builder.Services.AddSingleton((serviceProvider) =>
+ {
+ var handler = implementationFactory(serviceProvider);
+ return new EventHandlerDelegateWrapper(type, handler);
+ });
+
+ return builder;
+ }
+
///
/// Adds the to the OpenFeatureBuilder,
/// which manages the lifecycle of features within the application. It also allows
@@ -17,6 +382,7 @@ public static partial class OpenFeatureBuilderExtensions
/// The instance.
/// An optional action to configure .
/// The instance.
+ [Obsolete("Calling AddHostedFeatureLifecycle() is no longer necessary. OpenFeature will inject this automatically when you call AddOpenFeature().")]
public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null)
{
if (configureOptions is not null)
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.Hosting/OpenFeatureOptions.cs
similarity index 97%
rename from src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
rename to src/OpenFeature.Hosting/OpenFeatureOptions.cs
index e9cc3cb12..9d3dd818e 100644
--- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
+++ b/src/OpenFeature.Hosting/OpenFeatureOptions.cs
@@ -1,4 +1,4 @@
-namespace OpenFeature.DependencyInjection;
+namespace OpenFeature.Hosting;
///
/// Options to configure OpenFeature
diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs
similarity index 86%
rename from src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
rename to src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs
index 74d01ad3a..260b01319 100644
--- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs
+++ b/src/OpenFeature.Hosting/OpenFeatureServiceCollectionExtensions.cs
@@ -1,8 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
-using OpenFeature.DependencyInjection;
-using OpenFeature.DependencyInjection.Internal;
+using OpenFeature.Hosting;
+using OpenFeature.Hosting.Internal;
namespace OpenFeature;
@@ -30,6 +30,9 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services
var builder = new OpenFeatureBuilder(services);
configure(builder);
+ builder.Services.Configure(c => { }); // Ensures IOptions is available even when no providers are configured.
+ builder.Services.AddHostedService();
+
// If a default provider is specified without additional providers,
// return early as no extra configuration is needed.
if (builder.HasDefaultProvider && builder.DomainBoundProviderRegistrationCount == 0)
@@ -48,12 +51,13 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services
options.DefaultNameSelector = provider =>
{
var options = provider.GetRequiredService>().Value;
- return options.ProviderNames.First();
+ return options.ProviderNames.FirstOrDefault();
};
});
}
builder.AddPolicyBasedClient();
+
return services;
}
}
diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.Hosting/PolicyNameOptions.cs
similarity index 87%
rename from src/OpenFeature.DependencyInjection/PolicyNameOptions.cs
rename to src/OpenFeature.Hosting/PolicyNameOptions.cs
index f77b019b1..3dfa76f89 100644
--- a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs
+++ b/src/OpenFeature.Hosting/PolicyNameOptions.cs
@@ -1,4 +1,4 @@
-namespace OpenFeature.DependencyInjection;
+namespace OpenFeature.Hosting;
///
/// Options to configure the default feature client name.
diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs
similarity index 97%
rename from src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
rename to src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs
index d6346ad78..d63009d62 100644
--- a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs
+++ b/src/OpenFeature.Hosting/Providers/Memory/FeatureBuilderExtensions.cs
@@ -2,7 +2,7 @@
using Microsoft.Extensions.Options;
using OpenFeature.Providers.Memory;
-namespace OpenFeature.DependencyInjection.Providers.Memory;
+namespace OpenFeature.Hosting.Providers.Memory;
///
/// Extension methods for configuring feature providers with .
@@ -44,7 +44,7 @@ public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder bui
///
/// The instance for chaining.
public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Func?> flagsFactory)
- => AddInMemoryProvider(builder, domain, (provider, _) => flagsFactory(provider));
+ => builder.AddInMemoryProvider(domain, (provider, _) => flagsFactory(provider));
///
/// Adds an in-memory feature provider to the with a domain and contextual flag factory.
diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs b/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs
similarity index 91%
rename from src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs
rename to src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs
index ea5433f4e..3e7431eef 100644
--- a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderOptions.cs
+++ b/src/OpenFeature.Hosting/Providers/Memory/InMemoryProviderOptions.cs
@@ -1,6 +1,6 @@
using OpenFeature.Providers.Memory;
-namespace OpenFeature.DependencyInjection.Providers.Memory;
+namespace OpenFeature.Hosting.Providers.Memory;
///
/// Options for configuring the in-memory feature flag provider.
diff --git a/src/OpenFeature.Hosting/README.md b/src/OpenFeature.Hosting/README.md
new file mode 100644
index 000000000..3b530d214
--- /dev/null
+++ b/src/OpenFeature.Hosting/README.md
@@ -0,0 +1,141 @@
+# OpenFeature.Hosting
+
+[](https://www.nuget.org/packages/OpenFeature.Hosting)
+[](https://github.com/open-feature/spec/releases/tag/v0.8.0)
+
+OpenFeature.Hosting is an extension for the [OpenFeature .NET SDK](https://github.com/open-feature/dotnet-sdk) that streamlines integration with .NET applications using dependency injection and hosting. It enables seamless configuration and lifecycle management of feature flag providers, hooks, and evaluation context using idiomatic .NET patterns.
+
+**๐งช The OpenFeature.Hosting package is still considered experimental and may undergo significant changes. Feedback and contributions are welcome!**
+
+## ๐ Quick Start
+
+### Requirements
+
+- .NET 8+
+- .NET Framework 4.6.2+
+
+### Installation
+
+Add the package to your project:
+
+```sh
+dotnet add package OpenFeature.Hosting
+```
+
+### Basic Usage
+
+Register OpenFeature in your application's dependency injection container (e.g., in `Program.cs` for ASP.NET Core):
+
+```csharp
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddInMemoryProvider();
+});
+```
+
+You can add global evaluation context, hooks, and event handlers as needed:
+
+```csharp
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddContext((contextBuilder, serviceProvider) => {
+ // Custom context configuration
+ })
+ .AddHook()
+ .AddHandler(ProviderEventTypes.ProviderReady, (eventDetails) => {
+ // Handle provider ready event
+ });
+});
+```
+
+### Domain-Scoped Providers
+
+To register multiple providers and select a default provider by domain:
+
+```csharp
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddInMemoryProvider("default")
+ .AddInMemoryProvider("beta")
+ .AddPolicyName(options => {
+ options.DefaultNameSelector = serviceProvider => "default";
+ });
+});
+```
+
+### Registering a Custom Provider
+
+You can register a custom provider using a factory:
+
+```csharp
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder.AddProvider(provider => {
+ // Resolve services or configuration as needed
+ return new MyCustomProvider();
+ });
+});
+```
+
+## ๐งฉ Features
+
+- **Dependency Injection**: Register providers, hooks, and context using the .NET DI container.
+- **Domain Support**: Assign providers to logical domains for multi-tenancy or environment separation.
+- **Event Handlers**: React to provider lifecycle events (e.g., readiness).
+- **Extensibility**: Add custom hooks, context, and providers.
+
+## ๐ ๏ธ Example: ASP.NET Core Integration
+
+Below is a simple example of integrating OpenFeature with an ASP.NET Core application using an in-memory provider and a logging hook.
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddInMemoryProvider()
+ .AddHook();
+});
+
+var app = builder.Build();
+
+app.MapGet("/", async (IFeatureClient client) => {
+ bool enabled = await client.GetBooleanValueAsync("my-flag", false);
+ return enabled ? "Feature enabled!" : "Feature disabled.";
+});
+
+app.Run();
+```
+
+If you have multiple providers registered, you can specify which client and provider to resolve by using the `FromKeyedServices` attribute:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddOpenFeature(featureBuilder => {
+ featureBuilder
+ .AddInMemoryProvider("default")
+ .AddInMemoryProvider("beta")
+ .AddPolicyName(options => {
+ options.DefaultNameSelector = serviceProvider => "default";
+ });
+});
+
+var app = builder.Build();
+
+app.MapGet("/", async ([FromKeyedServices("beta")] IFeatureClient client) => {
+ bool enabled = await client.GetBooleanValueAsync("my-flag", false);
+ return enabled ? "Feature enabled!" : "Feature disabled.";
+});
+
+app.Run();
+```
+
+## ๐ Further Reading
+
+- [OpenFeature .NET SDK Documentation](https://github.com/open-feature/dotnet-sdk)
+- [OpenFeature Specification](https://openfeature.dev)
+- [Samples](https://github.com/open-feature/dotnet-sdk/blob/main/samples/AspNetCore/README.md)
+
+## ๐ค Contributing
+
+Contributions are welcome! See the [CONTRIBUTING](https://github.com/open-feature/dotnet-sdk/blob/main/CONTRIBUTING.md) guide for details.
diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs
new file mode 100644
index 000000000..12d61c253
--- /dev/null
+++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/FeatureBuilderExtensions.cs
@@ -0,0 +1,94 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using OpenFeature.Hosting;
+
+namespace OpenFeature.Providers.MultiProvider.DependencyInjection;
+
+///
+/// Extension methods for configuring the multi-provider with .
+///
+public static class FeatureBuilderExtensions
+{
+ ///
+ /// Adds a multi-provider to the with a configuration builder.
+ ///
+ /// The instance to configure.
+ ///
+ /// A delegate to configure the multi-provider using the .
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddMultiProvider(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ return builder.AddProvider(
+ serviceProvider => CreateMultiProviderFromConfigure(serviceProvider, configure));
+ }
+
+ ///
+ /// Adds a multi-provider with a specific domain to the with a configuration builder.
+ ///
+ /// The instance to configure.
+ /// The unique domain of the provider.
+ ///
+ /// A delegate to configure the multi-provider using the .
+ ///
+ /// The instance for chaining.
+ public static OpenFeatureBuilder AddMultiProvider(
+ this OpenFeatureBuilder builder,
+ string domain,
+ Action configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (string.IsNullOrWhiteSpace(domain))
+ {
+ throw new ArgumentException("Domain cannot be null or empty.", nameof(domain));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure), "Configure action cannot be null. Please provide a valid configuration for the multi-provider.");
+ }
+
+ return builder.AddProvider(
+ domain,
+ (serviceProvider, _) => CreateMultiProviderFromConfigure(serviceProvider, configure));
+ }
+
+ private static MultiProvider CreateMultiProviderFromConfigure(IServiceProvider serviceProvider, Action configure)
+ {
+ // Build the multi-provider configuration using the builder
+ var multiProviderBuilder = new MultiProviderBuilder();
+
+ // Apply the configuration action
+ configure(multiProviderBuilder);
+
+ // Build provider entries and strategy from the builder using the service provider
+ var providerEntries = multiProviderBuilder.BuildProviderEntries(serviceProvider);
+ var evaluationStrategy = multiProviderBuilder.BuildEvaluationStrategy(serviceProvider);
+
+ if (providerEntries.Count == 0)
+ {
+ throw new InvalidOperationException("At least one provider must be configured for the multi-provider.");
+ }
+
+ // Get logger from DI
+ var logger = serviceProvider.GetService>();
+
+ return new MultiProvider(providerEntries, evaluationStrategy, logger);
+ }
+}
diff --git a/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs
new file mode 100644
index 000000000..3353e6122
--- /dev/null
+++ b/src/OpenFeature.Providers.MultiProvider/DependencyInjection/MultiProviderBuilder.cs
@@ -0,0 +1,135 @@
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature.Providers.MultiProvider.Models;
+using OpenFeature.Providers.MultiProvider.Strategies;
+
+namespace OpenFeature.Providers.MultiProvider.DependencyInjection;
+
+///
+/// Builder for configuring a multi-provider with dependency injection.
+///
+public class MultiProviderBuilder
+{
+ private readonly List> _providerFactories = [];
+ private Func? _strategyFactory;
+
+ ///
+ /// Adds a provider to the multi-provider configuration using a factory method.
+ ///
+ /// The name for the provider.
+ /// A factory method to create the provider instance.
+ /// The instance for chaining.
+ public MultiProviderBuilder AddProvider(string name, Func factory)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Provider name cannot be null or empty.", nameof(name));
+ }
+
+ if (factory == null)
+ {
+ throw new ArgumentNullException(nameof(factory), "Provider configuration cannot be null.");
+ }
+
+ return AddProvider(name, sp => factory(sp));
+ }
+
+ ///
+ /// Adds a provider to the multi-provider configuration using a type.
+ ///
+ /// The type of the provider to add.
+ /// The name for the provider.
+ /// An optional factory method to create the provider instance. If not provided, the provider will be resolved from the service provider.
+ /// The instance for chaining.
+ public MultiProviderBuilder AddProvider(string name, Func? factory = null)
+ where TProvider : FeatureProvider
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Provider name cannot be null or empty.", nameof(name));
+ }
+
+ this._providerFactories.Add(sp =>
+ {
+ var provider = factory != null
+ ? factory(sp)
+ : sp.GetRequiredService();
+ return new ProviderEntry(provider, name);
+ });
+
+ return this;
+ }
+
+ ///
+ /// Adds a provider instance to the multi-provider configuration.
+ ///
+ /// The name for the provider.
+ /// The provider instance to add.
+ /// The instance for chaining.
+ public MultiProviderBuilder AddProvider(string name, FeatureProvider provider)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Provider name cannot be null or empty.", nameof(name));
+ }
+
+ if (provider == null)
+ {
+ throw new ArgumentNullException(nameof(provider), "Provider configuration cannot be null.");
+ }
+
+ return AddProvider(name, _ => provider);
+ }
+
+ ///
+ /// Sets the evaluation strategy for the multi-provider.
+ ///
+ /// The type of the evaluation strategy.
+ /// The instance for chaining.
+ public MultiProviderBuilder UseStrategy()
+ where TStrategy : BaseEvaluationStrategy, new()
+ {
+ return UseStrategy(static _ => new TStrategy());
+ }
+
+ ///
+ /// Sets the evaluation strategy for the multi-provider using a factory method.
+ ///
+ /// A factory method to create the strategy instance.
+ /// The instance for chaining.
+ public MultiProviderBuilder UseStrategy(Func factory)
+ {
+ this._strategyFactory = factory ?? throw new ArgumentNullException(nameof(factory), "Strategy for multi-provider cannot be null.");
+ return this;
+ }
+
+ ///
+ /// Sets the evaluation strategy for the multi-provider.
+ ///
+ /// The strategy instance to use.
+ /// The instance for chaining.
+ public MultiProviderBuilder UseStrategy(BaseEvaluationStrategy strategy)
+ {
+ if (strategy == null)
+ {
+ throw new ArgumentNullException(nameof(strategy));
+ }
+
+ return UseStrategy(_ => strategy);
+ }
+
+ ///
+ /// Builds the provider entries using the service provider.
+ ///
+ internal List BuildProviderEntries(IServiceProvider serviceProvider)
+ {
+ return this._providerFactories.Select(factory => factory(serviceProvider)).ToList();
+ }
+
+ ///
+ /// Builds the evaluation strategy using the service provider.
+ ///
+ internal BaseEvaluationStrategy? BuildEvaluationStrategy(IServiceProvider serviceProvider)
+ {
+ return this._strategyFactory?.Invoke(serviceProvider);
+ }
+}
diff --git a/src/OpenFeature.Providers.MultiProvider/Models/ChildProviderStatus.cs b/src/OpenFeature.Providers.MultiProvider/Models/ChildProviderStatus.cs
new file mode 100644
index 000000000..f66f8fae7
--- /dev/null
+++ b/src/OpenFeature.Providers.MultiProvider/Models/ChildProviderStatus.cs
@@ -0,0 +1,7 @@
+namespace OpenFeature.Providers.MultiProvider.Models;
+
+internal class ChildProviderStatus
+{
+ public string ProviderName { get; set; } = string.Empty;
+ public Exception? Error { get; set; }
+}
diff --git a/src/OpenFeature.Providers.MultiProvider/Models/ProviderEntry.cs b/src/OpenFeature.Providers.MultiProvider/Models/ProviderEntry.cs
new file mode 100644
index 000000000..da720da6c
--- /dev/null
+++ b/src/OpenFeature.Providers.MultiProvider/Models/ProviderEntry.cs
@@ -0,0 +1,29 @@
+namespace OpenFeature.Providers.MultiProvider.Models;
+
+///
+/// Represents an entry for a provider in the multi-provider configuration.
+///
+public class ProviderEntry
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The feature provider instance.
+ /// Optional custom name for the provider. If not provided, the provider's metadata name will be used.
+ public ProviderEntry(FeatureProvider provider, string? name = null)
+ {
+ this.Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ this.Name = name;
+ }
+
+ ///
+ /// Gets the feature provider instance.
+ ///
+ public FeatureProvider Provider { get; }
+
+ ///
+ /// Gets the optional custom name for the provider.
+ /// If null, the provider's metadata name should be used.
+ ///
+ public string? Name { get; }
+}
diff --git a/src/OpenFeature.Providers.MultiProvider/Models/RegisteredProvider.cs b/src/OpenFeature.Providers.MultiProvider/Models/RegisteredProvider.cs
new file mode 100644
index 000000000..ee62fd006
--- /dev/null
+++ b/src/OpenFeature.Providers.MultiProvider/Models/RegisteredProvider.cs
@@ -0,0 +1,41 @@
+namespace OpenFeature.Providers.MultiProvider.Models;
+
+internal class RegisteredProvider
+{
+#if NET9_0_OR_GREATER
+ private readonly Lock _statusLock = new();
+#else
+ private readonly object _statusLock = new object();
+#endif
+
+ private Constant.ProviderStatus _status = Constant.ProviderStatus.NotReady;
+
+ internal RegisteredProvider(FeatureProvider provider, string name)
+ {
+ this.Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ this.Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+
+ internal FeatureProvider Provider { get; }
+
+ internal string Name { get; }
+
+ internal Constant.ProviderStatus Status
+ {
+ get
+ {
+ lock (this._statusLock)
+ {
+ return this._status;
+ }
+ }
+ }
+
+ internal void SetStatus(Constant.ProviderStatus status)
+ {
+ lock (this._statusLock)
+ {
+ this._status = status;
+ }
+ }
+}
diff --git a/src/OpenFeature.Providers.MultiProvider/MultiProvider.cs b/src/OpenFeature.Providers.MultiProvider/MultiProvider.cs
new file mode 100644
index 000000000..e05ce9128
--- /dev/null
+++ b/src/OpenFeature.Providers.MultiProvider/MultiProvider.cs
@@ -0,0 +1,684 @@
+using System.Collections.Concurrent;
+using System.Collections.ObjectModel;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+using OpenFeature.Providers.MultiProvider.Models;
+using OpenFeature.Providers.MultiProvider.Strategies;
+using OpenFeature.Providers.MultiProvider.Strategies.Models;
+
+namespace OpenFeature.Providers.MultiProvider;
+
+///
+/// A feature provider that enables the use of multiple underlying providers, allowing different providers
+/// to be used for different flag keys or based on specific routing logic.
+///
+///
+/// The MultiProvider acts as a composite provider that can delegate flag resolution to different
+/// underlying providers based on configuration or routing rules. This enables scenarios where
+/// different feature flags may be served by different sources or providers within the same application.
+///
+/// Multi Provider specification
+public sealed partial class MultiProvider : FeatureProvider, IAsyncDisposable
+{
+ private readonly BaseEvaluationStrategy _evaluationStrategy;
+ private readonly IReadOnlyList _registeredProviders;
+ private readonly Metadata _metadata;
+
+ private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
+ private readonly SemaphoreSlim _shutdownSemaphore = new(1, 1);
+ private readonly object _providerStatusLock = new();
+ private ProviderStatus _providerStatus = ProviderStatus.NotReady;
+ // 0 = Not disposed, 1 = Disposed
+ // This is to handle the dispose pattern correctly with the async initialization and shutdown methods
+ private volatile int _disposed;
+
+ // Event handling infrastructure
+ private readonly ConcurrentDictionary _eventListeningTasks = new();
+ private readonly CancellationTokenSource _eventProcessingCancellation = new();
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class with the specified provider entries and evaluation strategy.
+ ///
+ /// A collection of provider entries containing the feature providers and their optional names.
+ /// The base evaluation strategy to use for determining how to evaluate features across multiple providers. If not specified, the first matching strategy will be used.
+ /// The logger for the client.
+ public MultiProvider(IEnumerable providerEntries, BaseEvaluationStrategy? evaluationStrategy = null, ILogger? logger = null)
+ {
+ if (providerEntries == null)
+ {
+ throw new ArgumentNullException(nameof(providerEntries));
+ }
+
+ var entries = providerEntries.ToList();
+ if (entries.Count == 0)
+ {
+ throw new ArgumentException("At least one provider entry must be provided.", nameof(providerEntries));
+ }
+
+ this._evaluationStrategy = evaluationStrategy ?? new FirstMatchStrategy();
+ this._registeredProviders = RegisterProviders(entries);
+
+ // Create aggregate metadata
+ this._metadata = new Metadata(MultiProviderConstants.ProviderName);
+
+ // Start listening to events from all registered providers
+ this.StartListeningToProviderEvents();
+
+ // Set logger
+ this._logger = logger ?? NullLogger.Instance;
+ }
+
+ ///
+ public override Metadata GetMetadata() => this._metadata;
+
+ ///
+ internal override ProviderStatus Status
+ {
+ get
+ {
+ lock (this._providerStatusLock)
+ {
+ return this._providerStatus;
+ }
+ }
+ set
+ {
+ lock (this._providerStatusLock)
+ {
+ this._providerStatus = value;
+ }
+ }
+ }
+
+ ///
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ ///
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ ///
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ ///
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ ///
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ ///
+ public override void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default)
+ {
+ if (this._disposed == 1)
+ {
+ throw new ObjectDisposedException(nameof(MultiProvider));
+ }
+
+ if (string.IsNullOrWhiteSpace(trackingEventName))
+ {
+ this.LogErrorTrackingEventEmptyName();
+ return;
+ }
+
+ foreach (var registeredProvider in this._registeredProviders)
+ {
+ var providerContext = new StrategyPerProviderContext