├── .editorconfig ├── .github ├── CODEOWNERS ├── component_owners.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── component-owners.yml │ ├── dco-merge-group.yml │ ├── dotnet-format.yml │ ├── lint-pr.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .release-please-manifest.json ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── DotnetSdkContrib.slnx ├── LICENSE ├── README.md ├── build ├── Common.prod.props ├── Common.props ├── Common.tests.props ├── openfeature-icon.png └── xunit.runner.json ├── global.json ├── nuget.config ├── release-please-config.json ├── renovate.json ├── src ├── Directory.Build.props ├── Directory.Build.targets ├── OpenFeature.Contrib.Hooks.Otel │ ├── CHANGELOG.md │ ├── MetricsConstants.cs │ ├── MetricsHook.cs │ ├── OpenFeature.Contrib.Hooks.Otel.csproj │ ├── OtelHook.cs │ ├── README.md │ ├── TracingHook.cs │ ├── assets │ │ ├── otlp-error.png │ │ └── otlp-success.png │ └── version.txt ├── OpenFeature.Contrib.Providers.ConfigCat │ ├── CHANGELOG.md │ ├── ConfigCatProvider.cs │ ├── OpenFeature.Contrib.Providers.ConfigCat.csproj │ ├── README.md │ └── UserBuilder.cs ├── OpenFeature.Contrib.Providers.EnvVar │ ├── CHANGELOG.md │ ├── EnvVarProvider.cs │ ├── OpenFeature.Contrib.Providers.EnvVar.csproj │ ├── README.md │ └── version.txt ├── OpenFeature.Contrib.Providers.FeatureManagement │ ├── CHANGELOG.md │ ├── FeatureManagementProvider.cs │ ├── OpenFeature.Contrib.Providers.FeatureManagement.csproj │ ├── README.md │ └── version.txt ├── OpenFeature.Contrib.Providers.Flagd │ ├── CHANGELOG.md │ ├── Cache.cs │ ├── DependencyInjection │ │ ├── FeatureBuilderExtensions.cs │ │ ├── FlagdProviderOptions.cs │ │ └── FlagdProviderOptionsExtensions.cs │ ├── FlagdConfig.cs │ ├── FlagdProvider.cs │ ├── OpenFeature.Contrib.Providers.Flagd.csproj │ ├── README.md │ ├── Resolver │ │ ├── InProcess │ │ │ ├── CustomEvaluators │ │ │ │ ├── FlagdProperties.cs │ │ │ │ ├── FractionalRule.cs │ │ │ │ ├── SemVerRule.cs │ │ │ │ └── StringRule.cs │ │ │ ├── InProcessResolver.cs │ │ │ ├── JsonEvaluator.cs │ │ │ └── JsonSchemaValidator.cs │ │ ├── Resolver.cs │ │ └── Rpc │ │ │ └── RpcResolver.cs │ ├── UnixDomainSocketConnectionFactory.cs │ ├── Utils │ │ └── CertificateLoader.cs │ └── version.txt ├── OpenFeature.Contrib.Providers.Flagsmith │ ├── CHANGELOG.md │ ├── FlagsmithProvider.cs │ ├── FlagsmithProviderConfiguration.cs │ ├── IFlagsmithProviderConfiguration.cs │ ├── OpenFeature.Contrib.Providers.Flagsmith.csproj │ ├── README.md │ └── version.txt ├── OpenFeature.Contrib.Providers.Flipt │ ├── CHANGELOG.md │ ├── ClientWrapper │ │ ├── FliptClientWrapper.cs │ │ └── IFliptClientWrapper.cs │ ├── Converters │ │ ├── JsonConverterExtensions.cs │ │ ├── OpenFeatureStructureConverter.cs │ │ └── OpenFeatureValueConverter.cs │ ├── FliptExtensions.cs │ ├── FliptProvider.cs │ ├── FliptToOpenFeatureConverter.cs │ ├── OpenFeature.Contrib.Providers.Flipt.csproj │ ├── README.md │ ├── openapi.yaml │ └── version.txt ├── OpenFeature.Contrib.Providers.GOFeatureFlag │ ├── CHANGELOG.md │ ├── GoFeatureFlagProvider.cs │ ├── GoFeatureFlagProviderOptions.cs │ ├── OpenFeature.Contrib.Providers.GOFeatureFlag.csproj │ ├── README.md │ ├── converters │ │ ├── DictionaryConverter.cs │ │ ├── JsonConverterExtensions.cs │ │ ├── OpenFeatureStructureConverter.cs │ │ └── OpenFeatureValueConverter.cs │ ├── exception │ │ ├── FlagDisabled.cs │ │ ├── FlagNotFoundError.cs │ │ ├── GeneralError.cs │ │ ├── GoFeatureFlagException.cs │ │ ├── ImpossibleToConvertTypeError.cs │ │ ├── InvalidEvaluationContext.cs │ │ ├── InvalidOption.cs │ │ ├── InvalidTargetingKey.cs │ │ ├── TypeMismatchError.cs │ │ └── UnauthorizedError.cs │ ├── extensions │ │ └── GoFeatureFlagExtensions.cs │ ├── hooks │ │ └── EnrichEvaluationContextHook.cs │ ├── models │ │ ├── ExporterMetadata.cs │ │ ├── OfrepRequest.cs │ │ └── OfrepResponse.cs │ └── version.txt └── OpenFeature.Contrib.Providers.Statsig │ ├── CHANGELOG.md │ ├── EvaluationContextExtensions.cs │ ├── OpenFeature.Contrib.Providers.Statsig.csproj │ ├── README.md │ ├── StatsigProvider.cs │ └── version.txt └── test ├── Directory.Build.props ├── OpenFeature.Contrib.Hooks.Otel.Test ├── MetricsHookTest.cs ├── OpenFeature.Contrib.Hooks.Otel.Test.csproj └── TracingHookTest.cs ├── OpenFeature.Contrib.Providers.ConfigCat.Test ├── ConfigCatProviderTest.cs ├── OpenFeature.Contrib.Providers.ConfigCat.Test.csproj └── UserBuilderTests.cs ├── OpenFeature.Contrib.Providers.EnvVar.Test ├── EnvVarProviderTests.cs └── OpenFeature.Contrib.Providers.EnvVar.Test.csproj ├── OpenFeature.Contrib.Providers.FeatureManagement.Test ├── FeatureManagementProviderSimpleFlagTest.cs ├── FeatureManagementProviderTestNoContext.cs ├── FeatureManagementProviderTestWithContext.cs ├── OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj ├── TestData.cs ├── appsettings.enabled.json └── appsettings.targeting.json ├── OpenFeature.Contrib.Providers.Flagd.E2e.Common ├── OpenFeature.Contrib.Providers.Flagd.E2e.Common.csproj └── Steps │ ├── EvaluationStepDefinitionBase.cs │ └── FlagdStepDefinitionBase.cs ├── OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest ├── Features │ ├── .gitignore │ └── .gitkeep ├── FlagdSyncTestBedContainer.cs ├── OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj └── Steps │ ├── EvaluationStepDefinitionsProcess.cs │ ├── FlagdStepDefinitionsProcess.cs │ └── TestHooks.cs ├── OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest ├── Features │ ├── .gitignore │ └── .gitkeep ├── FlagdRpcTestBedContainer.cs ├── OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj └── Steps │ ├── EvaluationStepDefinitionsRpc.cs │ ├── FlagdStepDefinitionsRpc.cs │ └── TestHooks.cs ├── OpenFeature.Contrib.Providers.Flagd.Test ├── CacheTest.cs ├── FeatureBuilderExtensionsTests.cs ├── FlagdConfigTest.cs ├── FlagdProviderOptionsExtensionsTests.cs ├── FlagdProviderTest.cs ├── FractionalEvaluatorTest.cs ├── JsonEvaluatorTest.cs ├── JsonSchemaValidatorTests.cs ├── OpenFeature.Contrib.Providers.Flagd.Test.csproj ├── SemVerEvaluatorTest.cs ├── StringEvaluatorTest.cs ├── Utils.cs └── mycert.pem ├── OpenFeature.Contrib.Providers.Flagsmith.Test ├── FlagsmithProviderTest.cs └── OpenFeature.Contrib.Providers.Flagsmith.Test.csproj ├── OpenFeature.Contrib.Providers.Flipt.Test ├── FliptExtensionsTest.cs ├── FliptProviderTest.cs ├── FliptToOpenFeatureConverterTest.cs └── OpenFeature.Contrib.Providers.Flipt.Test.csproj ├── OpenFeature.Contrib.Providers.GOFeatureFlag.Test ├── GoFeatureFlagProviderTest.cs ├── OfrepSerializationTest.cs └── OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj └── OpenFeature.Contrib.Providers.Statsig.Test ├── EvaluationContextExtensionsTests.cs ├── OpenFeature.Contrib.Providers.Statsig.Test.csproj └── StatsigProviderTest.cs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence 3 | # 4 | # Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-dotnet/workgroup.yaml 5 | # 6 | * @open-feature/sdk-dotnet-maintainers @open-feature/sdk-dotnet-approvers 7 | -------------------------------------------------------------------------------- /.github/component_owners.yml: -------------------------------------------------------------------------------- 1 | # Keep all in alphabetical order 2 | components: 3 | # src/ 4 | src/OpenFeature.Contrib.Hooks.Otel: 5 | - bacherfl 6 | - toddbaert 7 | - askpt 8 | src/OpenFeature.Contrib.Providers.Flagd: 9 | - bacherfl 10 | - toddbaert 11 | src/OpenFeature.Contrib.Providers.GOFeatureFlag: 12 | - thomaspoignant 13 | src/OpenFeature.Contrib.Providers.Flagsmith: 14 | - vpetrusevici 15 | - matthewelwell 16 | src/OpenFeature.Contrib.Providers.ConfigCat: 17 | - luizbon 18 | - adams85 19 | src/OpenFeature.Contrib.Providers.FeatureManagement: 20 | - ericpattison 21 | - toddbaert 22 | src/OpenFeature.Contrib.Providers.Statsig: 23 | - jenshenneberg 24 | - lattenborough 25 | src/OpenFeature.Contrib.Providers.Flipt: 26 | - jeandreidc 27 | - markphelps 28 | 29 | # test/ 30 | test/OpenFeature.Contrib.Hooks.Otel.Test: 31 | - bacherfl 32 | - toddbaert 33 | - askpt 34 | test/OpenFeature.Contrib.Providers.Flagd.Test: 35 | - bacherfl 36 | - toddbaert 37 | test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test: 38 | - thomaspoignant 39 | test/OpenFeature.Contrib.Providers.Flagsmith.Test: 40 | - vpetrusevici 41 | - matthewelwell 42 | test/OpenFeature.Contrib.Providers.ConfigCat.Test: 43 | - luizbon 44 | - adams85 45 | test/OpenFeature.Contrib.Providers.FeatureManagement.Test: 46 | - ericpattison 47 | - toddbaert 48 | test/src/OpenFeature.Contrib.Providers.Statsig.Test: 49 | - jenshenneberg 50 | - lattenborough 51 | test/src/OpenFeature.Contrib.Providers.Flipt.Test: 52 | - jeandreidc 53 | - markphelps 54 | 55 | ignored-authors: 56 | - renovate-bot 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - name 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | types: 11 | - opened 12 | - synchronize 13 | - reopened 14 | branches: 15 | - main 16 | merge_group: 17 | 18 | jobs: 19 | build: 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, windows-latest] 23 | 24 | runs-on: ${{ matrix.os }} 25 | 26 | permissions: 27 | contents: read 28 | pull-requests: write 29 | packages: read 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 34 | with: 35 | fetch-depth: 0 36 | submodules: recursive 37 | 38 | - name: Setup .NET SDK 39 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 40 | env: 41 | NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | global-json-file: global.json 44 | source-url: https://nuget.pkg.github.com/open-feature/index.json 45 | 46 | - name: Restore 47 | run: dotnet restore 48 | 49 | - name: Build 50 | run: dotnet build --no-restore 51 | 52 | - name: Test 53 | run: dotnet test --no-build --logger GitHubActions 54 | 55 | e2e: 56 | runs-on: ubuntu-latest 57 | permissions: 58 | contents: read 59 | pull-requests: write 60 | packages: read 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 64 | with: 65 | fetch-depth: 0 66 | submodules: recursive 67 | 68 | - name: Setup .NET SDK 69 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 70 | env: 71 | NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | with: 73 | global-json-file: global.json 74 | source-url: https://nuget.pkg.github.com/open-feature/index.json 75 | 76 | - name: Test 77 | run: dotnet build && E2E=true dotnet test --logger GitHubActions 78 | 79 | packaging: 80 | needs: build 81 | 82 | permissions: 83 | contents: read 84 | packages: write 85 | 86 | runs-on: ubuntu-latest 87 | 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 91 | with: 92 | fetch-depth: 0 93 | submodules: recursive 94 | 95 | - name: Setup .NET SDK 96 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 97 | env: 98 | NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | with: 100 | global-json-file: global.json 101 | source-url: https://nuget.pkg.github.com/open-feature/index.json 102 | 103 | - name: Restore 104 | run: dotnet restore 105 | 106 | - name: Pack NuGet packages (CI versions) 107 | if: startsWith(github.ref, 'refs/heads/') 108 | run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" 109 | 110 | - name: Pack NuGet packages (PR versions) 111 | if: startsWith(github.ref, 'refs/pull/') 112 | run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" 113 | 114 | - name: Publish NuGet packages (base) 115 | if: github.event.pull_request.head.repo.fork == false 116 | run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json --skip-duplicate 117 | 118 | - name: Publish NuGet packages (fork) 119 | if: github.event.pull_request.head.repo.fork == true 120 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 121 | with: 122 | name: nupkgs 123 | path: src/**/*.nupkg 124 | -------------------------------------------------------------------------------- /.github/workflows/component-owners.yml: -------------------------------------------------------------------------------- 1 | name: 'Component Owners' 2 | on: 3 | pull_request_target: 4 | 5 | permissions: 6 | contents: read # to read changed files 7 | issues: write # to read/write issue assignees 8 | pull-requests: write # to read/write PR reviewers 9 | 10 | jobs: 11 | run_self: 12 | runs-on: ubuntu-latest 13 | name: Auto Assign Owners 14 | steps: 15 | - uses: dyladan/component-owners@58bd86e9814d23f1525d0a970682cead459fa783 16 | with: 17 | config-file: .github/component_owners.yml 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/dco-merge-group.yml: -------------------------------------------------------------------------------- 1 | name: DCO 2 | on: 3 | merge_group: 4 | 5 | # Workaround because the DCO app doesn't run on a merge_group trigger 6 | # https://github.com/dcoapp/app/pull/200 7 | jobs: 8 | DCO: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | if: ${{ github.actor != 'renovate[bot]' }} 14 | steps: 15 | - run: echo "dummy DCO workflow (it won't run any check actually) to trigger by merge_group in order to enable merge queue" 16 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-format.yml: -------------------------------------------------------------------------------- 1 | name: dotnet format 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | check-format: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | packages: read 16 | 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | with: 21 | fetch-depth: 0 22 | submodules: recursive 23 | 24 | - name: Setup .NET 25 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 26 | env: 27 | NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | global-json-file: global.json 30 | source-url: https://nuget.pkg.github.com/open-feature/index.json 31 | 32 | - name: Restore 33 | run: dotnet restore 34 | 35 | - name: Build 36 | run: dotnet build --no-restore 37 | 38 | - name: dotnet format 39 | run: dotnet format --verify-no-changes --no-restore DotnetSdkContrib.slnx 40 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | runs-on: ubuntu-latest 17 | steps: 18 | - id: lint_pr_title 19 | uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 24 | # When the previous steps fails, the workflow would stop. By adding this 25 | # condition you can continue the execution with the populated error message. 26 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 27 | with: 28 | header: pr-title-lint-error 29 | message: | 30 | Hey there and thank you for opening this pull request! 👋🏼 31 | 32 | 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. 33 | Details: 34 | 35 | ``` 36 | ${{ steps.lint_pr_title.outputs.error_message }} 37 | ``` 38 | # Delete a previous comment when the issue has been resolved 39 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 40 | uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 41 | with: 42 | header: pr-title-lint-error 43 | delete: true 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Run Release Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release-package: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write # for googleapis/release-please-action to create release commit 13 | pull-requests: write # for googleapis/release-please-action to create release PR 14 | issues: write # for googleapis/release-please-action to create labels 15 | 16 | steps: 17 | - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 #v4 18 | id: release 19 | with: 20 | token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} 21 | outputs: 22 | all: ${{ toJSON(steps.release.outputs) }} 23 | releases_created: ${{ steps.release.outputs.releases_created }} 24 | paths_released: ${{ steps.release.outputs.paths_released }} 25 | 26 | release: 27 | needs: release-package 28 | environment: publish 29 | runs-on: ubuntu-latest 30 | permissions: 31 | id-token: write 32 | contents: write # upload sbom to a release 33 | attestations: write 34 | packages: read # for internal nuget reading 35 | if: ${{ fromJSON(needs.release-package.outputs.releases_created || false) }} 36 | strategy: 37 | matrix: 38 | release: ${{ fromJSON(needs.release-package.outputs.paths_released) }} 39 | env: 40 | TAG_NAME: ${{ fromJSON(needs.release-package.outputs.all)[format('{0}--tag_name', matrix.release)] }} 41 | 42 | steps: 43 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 44 | with: 45 | fetch-depth: 0 46 | submodules: recursive 47 | 48 | - name: Setup .NET SDK 49 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 50 | env: 51 | NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | global-json-file: global.json 54 | source-url: https://nuget.pkg.github.com/open-feature/index.json 55 | 56 | - name: Install dependencies 57 | run: dotnet restore 58 | 59 | - name: Build 60 | run: | 61 | dotnet build --configuration Release --no-restore -p:Deterministic=true 62 | 63 | - name: Pack 64 | run: | 65 | dotnet pack --configuration Release --no-build 66 | 67 | - name: Publish to Nuget 68 | run: | 69 | dotnet nuget push "${{ matrix.release }}/**/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_TOKEN }} 70 | 71 | - name: Generate artifact attestation 72 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 73 | with: 74 | subject-path: "${{ matrix.release }}/**/*.nupkg" 75 | 76 | - name: Find csproj file and set as environment variable 77 | id: find-csproj 78 | run: | 79 | csproj_path=$(find "${{ matrix.release }}" -name '*.csproj' -type f -print -quit) 80 | echo "CSPROJ_PATH=$csproj_path" >> "$GITHUB_OUTPUT" 81 | 82 | - name: Generate JSON SBOM 83 | uses: CycloneDX/gh-dotnet-generate-sbom@c183e4ac30e5b99354cb9a98c38548e07c538346 # v1.0.1 84 | with: 85 | path: ${{ steps.find-csproj.outputs.CSPROJ_PATH }} 86 | json: true 87 | github-bearer-token: ${{ secrets.GITHUB_TOKEN }} 88 | 89 | - name: Attest package 90 | uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0 91 | with: 92 | subject-path: "${{ matrix.release }}/**/*.nupkg" 93 | sbom-path: bom.json 94 | 95 | - name: Attach SBOM to artifact 96 | env: 97 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 98 | run: gh release upload ${{ env.TAG_NAME }} bom.json 99 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"] 2 | path = src/OpenFeature.Contrib.Providers.Flagd/schemas 3 | url = https://github.com/open-feature/schemas.git 4 | [submodule "spec"] 5 | path = spec 6 | url = https://github.com/open-feature/spec.git 7 | [submodule "src/OpenFeature.Contrib.Providers.Flagd/flagd-testbed"] 8 | path = src/OpenFeature.Contrib.Providers.Flagd/flagd-testbed 9 | url = https://github.com/open-feature/flagd-testbed.git 10 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/OpenFeature.Contrib.Hooks.Otel": "0.2.0", 3 | "src/OpenFeature.Contrib.Providers.Flagd": "0.3.2", 4 | "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.1", 5 | "src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.1", 6 | "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", 7 | "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.1", 8 | "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", 9 | "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5", 10 | "src/OpenFeature.Contrib.Providers.EnvVar": "0.0.7" 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "DotnetSdkContrib.slnx" 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## System Requirements 4 | 5 | Dotnet 8+ is recommended. 6 | 7 | ## Adding a project 8 | 9 | 1. Create a new library project under `src/`: `dotnet new classlib -o src/OpenFeature.Contrib.MyComponent` 10 | 2. Create a new test project under `test/`: `dotnet new xunit -o test/OpenFeature.Contrib.MyComponent.Test` 11 | 3. Add the library project to the solution: `dotnet sln DotnetSdkContrib.slnx add src/OpenFeature.Contrib.MyComponent/OpenFeature.Contrib.MyComponent.csproj` 12 | 4. Add the test project to the solution: `dotnet sln DotnetSdkContrib.slnx add test/OpenFeature.Contrib.MyComponent.Test/OpenFeature.Contrib.MyComponent.Test.csproj` 13 | 5. Add the desired properties to your library's `.csproj` file (see example below). 14 | 6. Remove all content besides the root element from your test project's `.csproj` file (all settings will be inherited). 15 | 7. Add the new library project to `release-please-config.json`. 16 | 8. Add a `version.txt` file to the root of your library with a version matching that in your new `.csproj` file, e.g. `0.0.1`. 17 | 9. If you care to release a pre `1.0.0` version, add the same version above to `.release-please-manifest.json`. Failing to do this will release a `1.0.0` initial release. 18 | 19 | Sample `.csproj` file: 20 | 21 | ```xml 22 | 23 | 24 | 25 | OpenFeature.Contrib.MyComponent 26 | 0.0.1 27 | $(VersionNumber) 28 | $(VersionNumber) 29 | $(VersionNumber) 30 | A very valuable OpenFeature contribution! 31 | Me! 32 | 33 | 34 | 35 | ``` 36 | 37 | ## Documentation 38 | 39 | Any published modules must have documentation in their root directory, explaining the basic purpose of the module as well as installation and usage instructions. 40 | Instructions for how to develop a module should also be included (required system dependencies, instructions for testing locally, etc). 41 | 42 | ## Testing 43 | 44 | Any published modules must have reasonable test coverage. 45 | The instructions above will generate a test project for you. 46 | 47 | Use `dotnet test` to test the entire project. 48 | 49 | ## Versioning and releasing 50 | 51 | This repo uses _Release Please_ to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), generated when [PRs are merged](https://github.com/amannn/action-semantic-pull-request). When Release Please's running PR is merged, any changed artifacts are published. 52 | 53 | ## Dependencies 54 | 55 | Keep dependencies to a minimum. 56 | Dependencies used only for building and testing should have a `all` element to prevent them from being exposed to consumers. 57 | 58 | ## Consuming pre-release packages 59 | 60 | 1. Acquire a [GitHub personal access token (PAT)](https://docs.github.com/github/authenticating-to-github/creating-a-personal-access-token) scoped for `read:packages` and verify the permissions: 61 | 62 | ```console 63 | $ gh auth login --scopes read:packages 64 | 65 | ? What account do you want to log into? GitHub.com 66 | ? What is your preferred protocol for Git operations? HTTPS 67 | ? How would you like to authenticate GitHub CLI? Login with a web browser 68 | 69 | ! First copy your one-time code: ****-**** 70 | Press Enter to open github.com in your browser... 71 | 72 | ✓ Authentication complete. 73 | - gh config set -h github.com git_protocol https 74 | ✓ Configured git protocol 75 | ✓ Logged in as ******** 76 | ``` 77 | 78 | ```console 79 | $ gh auth status 80 | 81 | github.com 82 | ✓ Logged in to github.com as ******** (~/.config/gh/hosts.yml) 83 | ✓ Git operations for github.com configured to use https protocol. 84 | ✓ Token: gho_************************************ 85 | ✓ Token scopes: gist, read:org, read:packages, repo, workflow 86 | ``` 87 | 88 | 2. Run the following command to configure your local environment to consume packages from GitHub Packages: 89 | 90 | ```console 91 | $ dotnet nuget update source github-open-feature --username $(gh api user --jq .email) --password $(gh auth token) --store-password-in-clear-text 92 | 93 | Package source "github-open-feature" was successfully updated. 94 | ``` 95 | -------------------------------------------------------------------------------- /DotnetSdkContrib.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature .NET Contributions 2 | 3 | This repository is intended for OpenFeature contributions which are not included in the [OpenFeature SDK](https://github.com/open-feature/dotnet-sdk). 4 | 5 | ## Contributing 6 | 7 | Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) guide. 8 | 9 | ## Useful links 10 | 11 | * For more information on OpenFeature, visit [openfeature.dev](https://openfeature.dev) 12 | * For help or feedback on this project, join us on [Slack][slack] or create a [GitHub issue][github-issue]. 13 | 14 | ## License 15 | 16 | [Apache License 2.0](./LICENSE) 17 | 18 | [github-issue]: https://github.com/open-feature/dotnet-sdk-contrib/issues/new 19 | [slack]: https://cloud-native.slack.com/archives/C0344AANLA1 20 | -------------------------------------------------------------------------------- /build/Common.prod.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | true 7 | true 8 | true 9 | 10 | 11 | 12 | netstandard2.0;net462;net8.0;net9.0 13 | git 14 | https://github.com/open-feature/dotnet-sdk-contrib 15 | OpenFeature is an open specification that provides a vendor-agnostic, 16 | community-driven API for feature flagging that works with your favorite feature flag 17 | management tool or in-house solution. 18 | Feature;OpenFeature;Flags; 19 | openfeature-icon.png 20 | https://openfeature.dev 21 | Apache-2.0 22 | OpenFeature Authors 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /build/Common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | true 5 | true 6 | all 7 | low 8 | 9 | 10 | 11 | full 12 | true 13 | 14 | 15 | 16 | true 17 | 18 | 19 | 20 | 22 | 23 | -------------------------------------------------------------------------------- /build/Common.tests.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(NoWarn);1591 6 | false 7 | net8.0;net9.0 8 | $(TargetFrameworks);net462 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | $(NoWarn);CA2007 17 | 18 | 19 | 20 | 22 | PreserveNewest 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 42 | runtime; build; native; contentfiles; analyzers; buildtransitive 43 | all 44 | 45 | 46 | 48 | 49 | 51 | 52 | 53 | 54 | 58 | [4.18.1] 59 | [3.1.2] 60 | [2.2.1] 61 | [2.3.3] 62 | [17.13.0] 63 | [5.0.0] 64 | [4.3.1] 65 | [2.8.2,3.0) 66 | [2.9.3,3.0) 67 | 68 | -------------------------------------------------------------------------------- /build/openfeature-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/dotnet-sdk-contrib/741bfc0d8c9a2ab33bfe658b2279cbaae018779f/build/openfeature-icon.png -------------------------------------------------------------------------------- /build/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxParallelThreads": 1, 3 | "parallelizeTestCollections": false 4 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "rollForward": "latestFeature", 4 | "version": "9.0.203", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>open-feature/community-tooling" 5 | ], 6 | "dependencyDashboardApproval": true, 7 | "recreateWhen": "never" 8 | } 9 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.8...OpenFeature.Contrib.Hooks.Otel-v0.2.0) (2024-08-22) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) 9 | 10 | ### ✨ New Features 11 | 12 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) ([f845134](https://github.com/open-feature/dotnet-sdk-contrib/commit/f84513438586457087ac47fd40629912f2ec473a)) 13 | 14 | ## [0.1.8](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.7...OpenFeature.Contrib.Hooks.Otel-v0.1.8) (2024-08-21) 15 | 16 | 17 | ### 🐛 Bug Fixes 18 | 19 | * version expression ([cad2cd1](https://github.com/open-feature/dotnet-sdk-contrib/commit/cad2cd166d0c25753b37189f044c3a585cda0fad)) 20 | 21 | ## [0.1.7](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.6...OpenFeature.Contrib.Hooks.Otel-v0.1.7) (2024-08-20) 22 | 23 | 24 | ### 🧹 Chore 25 | 26 | * update OpenFeature version compatiblity ([#249](https://github.com/open-feature/dotnet-sdk-contrib/issues/249)) ([232e948](https://github.com/open-feature/dotnet-sdk-contrib/commit/232e948a0916ca10612f85343e2eecebca107090)) 27 | 28 | ## [0.1.6](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.5...OpenFeature.Contrib.Hooks.Otel-v0.1.6) (2024-07-25) 29 | 30 | 31 | ### 🧹 Chore 32 | 33 | * **deps:** update dependency opentelemetry.api to v1.9.0 ([#170](https://github.com/open-feature/dotnet-sdk-contrib/issues/170)) ([258fe9a](https://github.com/open-feature/dotnet-sdk-contrib/commit/258fe9aadbc8dfe834accf4dbbe82ff601abd73d)) 34 | 35 | ## [0.1.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.4...OpenFeature.Contrib.Hooks.Otel-v0.1.5) (2024-07-19) 36 | 37 | 38 | ### 🧹 Chore 39 | 40 | * **deps:** update dependency opentelemetry.api to v1.7.0 ([#65](https://github.com/open-feature/dotnet-sdk-contrib/issues/65)) ([533dfa6](https://github.com/open-feature/dotnet-sdk-contrib/commit/533dfa652011b645d48c5d17dbdfe39b64a8eadf)) 41 | 42 | ## [0.1.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.3...OpenFeature.Contrib.Hooks.Otel-v0.1.4) (2024-01-25) 43 | 44 | 45 | ### 🧹 Chore 46 | 47 | * Add support for GitHub Packages ([#134](https://github.com/open-feature/dotnet-sdk-contrib/issues/134)) ([0def0da](https://github.com/open-feature/dotnet-sdk-contrib/commit/0def0da173e2f327b7381eba043b6e99ae8f26fe)) 48 | 49 | ## [0.1.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.2...OpenFeature.Contrib.Hooks.Otel-v0.1.3) (2024-01-11) 50 | 51 | 52 | ### 🔄 Refactoring 53 | 54 | * Reduce OTel dependencies ([#132](https://github.com/open-feature/dotnet-sdk-contrib/issues/132)) ([de86b0e](https://github.com/open-feature/dotnet-sdk-contrib/commit/de86b0e34ea829608360109a5c5f1c61f8efdcaf)) 55 | 56 | ## [0.1.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.1...OpenFeature.Contrib.Hooks.Otel-v0.1.2) (2023-12-19) 57 | 58 | 59 | ### ✨ New Features 60 | 61 | * Add Metrics Hook ([#114](https://github.com/open-feature/dotnet-sdk-contrib/issues/114)) ([5845e2b](https://github.com/open-feature/dotnet-sdk-contrib/commit/5845e2b0ae4b89a8a313051b42e6afdd856f1ea3)) 62 | 63 | 64 | ### 🔄 Refactoring 65 | 66 | * Replace OtelHook with TracingHook. Deprecating OtelHook ([#116](https://github.com/open-feature/dotnet-sdk-contrib/issues/116)) ([755c549](https://github.com/open-feature/dotnet-sdk-contrib/commit/755c54960bccac97f6836ea8371d75bc2f1a02bb)) 67 | 68 | ## [0.1.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.1.0...OpenFeature.Contrib.Hooks.Otel-v0.1.1) (2023-03-22) 69 | 70 | 71 | ### Features 72 | 73 | * added OTel hook ([#44](https://github.com/open-feature/dotnet-sdk-contrib/issues/44)) ([cbe92b5](https://github.com/open-feature/dotnet-sdk-contrib/commit/cbe92b52a3f58279d57a054bed368b6003e03561)) 74 | 75 | ## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Hooks.Otel-v0.0.2...OpenFeature.Contrib.Hooks.Otel-v0.1.0) (2022-10-16) 76 | 77 | 78 | ### ⚠ BREAKING CHANGES 79 | 80 | * rename namespace, add OpenFeature dependency and readmes. 81 | * rename namespace, add OpenFeature dep (#18) 82 | 83 | ### Features 84 | 85 | * rename namespace, add OpenFeature dep ([#18](https://github.com/open-feature/dotnet-sdk-contrib/issues/18)) ([3ca3172](https://github.com/open-feature/dotnet-sdk-contrib/commit/3ca31722b83053d4edf2038889c78efa717a7cff)) 86 | * rename namespace, add OpenFeature dependency and readmes. ([3ca3172](https://github.com/open-feature/dotnet-sdk-contrib/commit/3ca31722b83053d4edf2038889c78efa717a7cff)) 87 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs: -------------------------------------------------------------------------------- 1 | namespace OpenFeature.Contrib.Hooks.Otel; 2 | 3 | internal static class MetricsConstants 4 | { 5 | internal const string ActiveCountName = "feature_flag.evaluation_active_count"; 6 | internal const string RequestsTotalName = "feature_flag.evaluation_requests_total"; 7 | internal const string SuccessTotalName = "feature_flag.evaluation_success_total"; 8 | internal const string ErrorTotalName = "feature_flag.evaluation_error_total"; 9 | 10 | internal const string ActiveDescription = "active flag evaluations counter"; 11 | internal const string RequestsDescription = "feature flag evaluation request counter"; 12 | internal const string SuccessDescription = "feature flag evaluation success counter"; 13 | internal const string ErrorDescription = "feature flag evaluation error counter"; 14 | 15 | internal const string KeyAttr = "key"; 16 | internal const string ProviderNameAttr = "provider_name"; 17 | internal const string VariantAttr = "variant"; 18 | internal const string ReasonAttr = "reason"; 19 | internal const string ExceptionAttr = "exception"; 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.Metrics; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using OpenFeature.Model; 9 | 10 | namespace OpenFeature.Contrib.Hooks.Otel; 11 | 12 | /// 13 | /// Represents a hook for capturing metrics related to flag evaluations. 14 | /// The meter name is "OpenFeature.Contrib.Hooks.Otel". 15 | /// 16 | public class MetricsHook : Hook 17 | { 18 | private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName(); 19 | private static readonly string InstrumentationName = AssemblyName.Name; 20 | private static readonly string InstrumentationVersion = AssemblyName.Version?.ToString(); 21 | 22 | private readonly UpDownCounter _evaluationActiveUpDownCounter; 23 | private readonly Counter _evaluationRequestCounter; 24 | private readonly Counter _evaluationSuccessCounter; 25 | private readonly Counter _evaluationErrorCounter; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | public MetricsHook() 31 | { 32 | var meter = new Meter(InstrumentationName, InstrumentationVersion); 33 | 34 | _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription); 35 | _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription); 36 | _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription); 37 | _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription); 38 | } 39 | 40 | /// 41 | public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 42 | { 43 | var tagList = new TagList 44 | { 45 | { MetricsConstants.KeyAttr, context.FlagKey }, 46 | { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name } 47 | }; 48 | 49 | _evaluationActiveUpDownCounter.Add(1, tagList); 50 | _evaluationRequestCounter.Add(1, tagList); 51 | 52 | return base.BeforeAsync(context, hints); 53 | } 54 | 55 | 56 | /// 57 | public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 58 | { 59 | var tagList = new TagList 60 | { 61 | { MetricsConstants.KeyAttr, context.FlagKey }, 62 | { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, 63 | { MetricsConstants.VariantAttr, details.Variant ?? details.Value?.ToString() }, 64 | { MetricsConstants.ReasonAttr, details.Reason ?? "UNKNOWN" } 65 | }; 66 | 67 | _evaluationSuccessCounter.Add(1, tagList); 68 | 69 | return base.AfterAsync(context, details, hints); 70 | } 71 | 72 | /// 73 | public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 74 | { 75 | var tagList = new TagList 76 | { 77 | { MetricsConstants.KeyAttr, context.FlagKey }, 78 | { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, 79 | { MetricsConstants.ExceptionAttr, error?.Message ?? "Unknown error" } 80 | }; 81 | 82 | _evaluationErrorCounter.Add(1, tagList); 83 | 84 | return base.ErrorAsync(context, error, hints); 85 | } 86 | 87 | /// 88 | public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 89 | { 90 | var tagList = new TagList 91 | { 92 | { MetricsConstants.KeyAttr, context.FlagKey }, 93 | { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name } 94 | }; 95 | 96 | _evaluationActiveUpDownCounter.Add(-1, tagList); 97 | 98 | return base.FinallyAsync(context, hints); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/OpenFeature.Contrib.Hooks.Otel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenFeature.Contrib.Hooks.Otel 5 | 0.2.0 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | Open Telemetry Hook for .NET 10 | Florian Bacher 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/OtelHook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using OpenFeature.Model; 7 | 8 | namespace OpenFeature.Contrib.Hooks.Otel; 9 | 10 | 11 | /// 12 | /// Stub. 13 | /// 14 | [ExcludeFromCodeCoverage] 15 | [Obsolete("This class is obsolete and will be removed in a future version. Please use TracingHook instead.")] 16 | public class OtelHook : Hook 17 | { 18 | private readonly TracingHook _tracingHook = new TracingHook(); 19 | 20 | /// 21 | public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, 22 | IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 23 | { 24 | _tracingHook.AfterAsync(context, details, hints); 25 | 26 | return default; 27 | } 28 | 29 | /// 30 | public override ValueTask ErrorAsync(HookContext context, Exception error, 31 | IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 32 | { 33 | _tracingHook.ErrorAsync(context, error, hints); 34 | 35 | return default; 36 | } 37 | 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/TracingHook.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using OpenFeature.Model; 6 | using OpenTelemetry.Trace; 7 | 8 | namespace OpenFeature.Contrib.Hooks.Otel; 9 | 10 | 11 | /// 12 | /// Stub. 13 | /// 14 | public class TracingHook : Hook 15 | { 16 | 17 | /// 18 | public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, 19 | IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 20 | { 21 | Activity.Current? 22 | .SetTag("feature_flag.key", details.FlagKey) 23 | .SetTag("feature_flag.variant", details.Variant) 24 | .SetTag("feature_flag.provider_name", context.ProviderMetadata.Name) 25 | .AddEvent(new ActivityEvent("feature_flag", tags: new ActivityTagsCollection 26 | { 27 | ["feature_flag.key"] = details.FlagKey, 28 | ["feature_flag.variant"] = details.Variant, 29 | ["feature_flag.provider_name"] = context.ProviderMetadata.Name 30 | })); 31 | 32 | return default; 33 | } 34 | 35 | /// 36 | public override ValueTask ErrorAsync(HookContext context, System.Exception error, 37 | IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 38 | { 39 | Activity.Current?.RecordException(error); 40 | 41 | return default; 42 | } 43 | 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/assets/otlp-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/dotnet-sdk-contrib/741bfc0d8c9a2ab33bfe658b2279cbaae018779f/src/OpenFeature.Contrib.Hooks.Otel/assets/otlp-error.png -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/assets/otlp-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/dotnet-sdk-contrib/741bfc0d8c9a2ab33bfe658b2279cbaae018779f/src/OpenFeature.Contrib.Hooks.Otel/assets/otlp-success.png -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Hooks.Otel/version.txt: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.1.0...OpenFeature.Contrib.Providers.ConfigCat-v0.1.1) (2024-09-17) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * Revise ConfigCat provider ([#280](https://github.com/open-feature/dotnet-sdk-contrib/issues/280)) ([0b2d5f2](https://github.com/open-feature/dotnet-sdk-contrib/commit/0b2d5f29490ad16ee5efde55d31354e0322c6f86)) 9 | 10 | ## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.5...OpenFeature.Contrib.Providers.ConfigCat-v0.1.0) (2024-08-22) 11 | 12 | 13 | ### ⚠ BREAKING CHANGES 14 | 15 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) 16 | 17 | ### ✨ New Features 18 | 19 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) ([f845134](https://github.com/open-feature/dotnet-sdk-contrib/commit/f84513438586457087ac47fd40629912f2ec473a)) 20 | 21 | ## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.4...OpenFeature.Contrib.Providers.ConfigCat-v0.0.5) (2024-08-21) 22 | 23 | 24 | ### 🐛 Bug Fixes 25 | 26 | * version expression ([cad2cd1](https://github.com/open-feature/dotnet-sdk-contrib/commit/cad2cd166d0c25753b37189f044c3a585cda0fad)) 27 | 28 | ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.3...OpenFeature.Contrib.Providers.ConfigCat-v0.0.4) (2024-08-20) 29 | 30 | 31 | ### 🧹 Chore 32 | 33 | * update OpenFeature version compatiblity ([#249](https://github.com/open-feature/dotnet-sdk-contrib/issues/249)) ([232e948](https://github.com/open-feature/dotnet-sdk-contrib/commit/232e948a0916ca10612f85343e2eecebca107090)) 34 | 35 | ## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.2...OpenFeature.Contrib.Providers.ConfigCat-v0.0.3) (2024-06-27) 36 | 37 | 38 | ### 🧹 Chore 39 | 40 | * Add support for GitHub Packages ([#134](https://github.com/open-feature/dotnet-sdk-contrib/issues/134)) ([0def0da](https://github.com/open-feature/dotnet-sdk-contrib/commit/0def0da173e2f327b7381eba043b6e99ae8f26fe)) 41 | 42 | 43 | ### 🔄 Refactoring 44 | 45 | * Use the newly introduced ConfigCat error codes to determine error type ([#201](https://github.com/open-feature/dotnet-sdk-contrib/issues/201)) ([a9eef05](https://github.com/open-feature/dotnet-sdk-contrib/commit/a9eef0559d2eb2ab53249c585ddae5ad74c98328)) 46 | 47 | ## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.1...OpenFeature.Contrib.Providers.ConfigCat-v0.0.2) (2024-01-16) 48 | 49 | 50 | ### ✨ New Features 51 | 52 | * Add ConfigCat provider ([#119](https://github.com/open-feature/dotnet-sdk-contrib/issues/119)) ([20aeb3a](https://github.com/open-feature/dotnet-sdk-contrib/commit/20aeb3a471227571fdc47a46a6292e0b59c9b3a5)) 53 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenFeature.Contrib.Providers.ConfigCat 5 | 0.1.1 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | ConfigCat provider for .NET 10 | Luiz Bon 11 | true 12 | 13 | 14 | 15 | 16 | <_Parameter1>$(MSBuildProjectName).Test 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.ConfigCat/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat Feature Flag .NET Provider 2 | 3 | The ConfigCat Flag provider allows you to connect to your ConfigCat instance. 4 | 5 | # .NET SDK usage 6 | 7 | ## Requirements 8 | 9 | - open-feature/dotnet-sdk v1.5.0 > v2.0.0 10 | 11 | ## Install dependencies 12 | 13 | The first things we will do is install the **Open Feature SDK** and the **ConfigCat Feature Flag provider**. 14 | 15 | ### .NET Cli 16 | ```shell 17 | dotnet add package OpenFeature.Contrib.Providers.ConfigCat 18 | ``` 19 | ### Package Manager 20 | 21 | ```shell 22 | NuGet\Install-Package OpenFeature.Contrib.Providers.ConfigCat 23 | ``` 24 | ### Package Reference 25 | 26 | ```xml 27 | 28 | ``` 29 | ### Packet cli 30 | 31 | ```shell 32 | paket add OpenFeature.Contrib.Providers.ConfigCat 33 | ``` 34 | 35 | ### Cake 36 | 37 | ```shell 38 | // Install OpenFeature.Contrib.Providers.ConfigCat as a Cake Addin 39 | #addin nuget:?package=OpenFeature.Contrib.Providers.ConfigCat 40 | 41 | // Install OpenFeature.Contrib.Providers.ConfigCat as a Cake Tool 42 | #tool nuget:?package=OpenFeature.Contrib.Providers.ConfigCat 43 | ``` 44 | 45 | ## Using the ConfigCat Provider with the OpenFeature SDK 46 | 47 | The following example shows how to use the ConfigCat provider with the OpenFeature SDK. 48 | 49 | ```csharp 50 | using System; 51 | using ConfigCat.Client; 52 | using OpenFeature.Contrib.ConfigCat; 53 | 54 | var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#"); 55 | 56 | // Set the configCatProvider as the provider for the OpenFeature SDK 57 | await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider); 58 | 59 | var client = OpenFeature.Api.Instance.GetClient(); 60 | 61 | var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false); 62 | if (isAwesomeFeatureEnabled) 63 | { 64 | doTheNewThing(); 65 | } 66 | else 67 | { 68 | doTheOldThing(); 69 | } 70 | ``` 71 | 72 | ### Customizing the ConfigCat Provider 73 | 74 | The ConfigCat provider can be customized by passing a callback setting up a `ConfigCatClientOptions` object to the constructor. 75 | 76 | ```csharp 77 | Action configureConfigCatOptions = (options) => 78 | { 79 | options.PollingMode = PollingModes.LazyLoad(cacheTimeToLive: TimeSpan.FromSeconds(10)); 80 | options.Logger = new ConsoleLogger(LogLevel.Info); 81 | // ... 82 | }; 83 | 84 | var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configureConfigCatOptions); 85 | ``` 86 | 87 | For a full list of options see the [ConfigCat documentation](https://configcat.com/docs/sdk-reference/dotnet/). 88 | 89 | ### Cleaning up 90 | 91 | On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client. 92 | 93 | ```csharp 94 | await OpenFeature.Api.Instance.ShutdownAsync(); 95 | ``` 96 | 97 | ## EvaluationContext and ConfigCat User Object relationship 98 | 99 | An evaluation context in the OpenFeature specification is a container for arbitrary contextual data that can be used as a basis for feature flag evaluation. 100 | The ConfigCat provider translates these evaluation contexts to ConfigCat [User Objects](https://configcat.com/docs/targeting/user-object/). 101 | 102 | The ConfigCat User Object has a few pre-defined attributes that can be used to evaluate a flag. These are: 103 | 104 | | Attribute | Description | 105 | |--------------|----------------------------------------------------------------------------------------------------------------| 106 | | `Identifier` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. | 107 | | `Email` | The email address of the user. | 108 | | `Country` | The country of the user. | 109 | 110 | Since `EvaluationContext` is a simple dictionary, the provider will try to match the keys to ConfigCat user attributes following the table below in a case-insensitive manner. 111 | 112 | | EvaluationContext Key | ConfigCat User Attribute | 113 | |-----------------------|--------------------------| 114 | | `id` | `Identifier` | 115 | | `identifier` | `Identifier` | 116 | | `email` | `Email` | 117 | | `country` | `Country` | 118 | | Any other | `Custom` | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using ConfigCat.Client; 4 | using OpenFeature.Model; 5 | 6 | namespace OpenFeature.Contrib.ConfigCat; 7 | 8 | internal static class UserBuilder 9 | { 10 | private static readonly string[] PossibleUserIds = { "ID", "IDENTIFIER" }; 11 | 12 | internal static User BuildUser(this EvaluationContext context) 13 | { 14 | if (context == null) 15 | { 16 | return null; 17 | } 18 | 19 | var user = new User(context.GetUserId()); 20 | 21 | foreach (var value in context) 22 | { 23 | if (StringComparer.OrdinalIgnoreCase.Equals("EMAIL", value.Key)) 24 | { 25 | user.Email = value.Value.AsString; 26 | } 27 | else if (StringComparer.OrdinalIgnoreCase.Equals("COUNTRY", value.Key)) 28 | { 29 | user.Country = value.Value.AsString; 30 | } 31 | else 32 | { 33 | user.Custom.Add(value.Key, value.Value.AsString); 34 | } 35 | } 36 | 37 | return user; 38 | } 39 | 40 | private static string GetUserId(this EvaluationContext context) 41 | { 42 | var pair = context.AsDictionary().FirstOrDefault(x => PossibleUserIds.Contains(x.Key, StringComparer.OrdinalIgnoreCase)); 43 | 44 | return pair.Key != null ? pair.Value.AsString : ""; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.7](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.6...OpenFeature.Contrib.Providers.EnvVar-v0.0.7) (2025-05-08) 4 | 5 | 6 | ### 🧹 Chore 7 | 8 | * fix typo in readme ([#400](https://github.com/open-feature/dotnet-sdk-contrib/issues/400)) ([4ea76fc](https://github.com/open-feature/dotnet-sdk-contrib/commit/4ea76fc18474a054d6725d17833cce5d895714b1)) 9 | 10 | ## [0.0.6](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.5...OpenFeature.Contrib.Providers.EnvVar-v0.0.6) (2025-05-08) 11 | 12 | 13 | ### 🧹 Chore 14 | 15 | * relax doc requirements for test projects ([e204e16](https://github.com/open-feature/dotnet-sdk-contrib/commit/e204e168ee8ccda34f46de325d45447e3ef85f73)) 16 | 17 | ## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.4...OpenFeature.Contrib.Providers.EnvVar-v0.0.5) (2025-05-08) 18 | 19 | 20 | ### 🧹 Chore 21 | 22 | * typo in readme ([e1c1450](https://github.com/open-feature/dotnet-sdk-contrib/commit/e1c1450e0c237764485d84e90a8573c2bcdb2edd)) 23 | 24 | ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.3...OpenFeature.Contrib.Providers.EnvVar-v0.0.4) (2025-05-08) 25 | 26 | 27 | ### 🧹 Chore 28 | 29 | * update README ([ed1c3ae](https://github.com/open-feature/dotnet-sdk-contrib/commit/ed1c3ae133df6a4ee6949dee3b3e441ef2dafea1)) 30 | 31 | ## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.2...OpenFeature.Contrib.Providers.EnvVar-v0.0.3) (2025-05-06) 32 | 33 | 34 | ### 🧹 Chore 35 | 36 | * Add .editorconfig and cleanup code ([#375](https://github.com/open-feature/dotnet-sdk-contrib/issues/375)) ([683a392](https://github.com/open-feature/dotnet-sdk-contrib/commit/683a392604aca6c9a92b1f64fa30bc9e3e069b4f)) 37 | 38 | ## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.1...OpenFeature.Contrib.Providers.EnvVar-v0.0.2) (2025-03-12) 39 | 40 | 41 | ### ✨ New Features 42 | 43 | * Environment Variable Provider ([#312](https://github.com/open-feature/dotnet-sdk-contrib/issues/312)) ([4908000](https://github.com/open-feature/dotnet-sdk-contrib/commit/4908000ed27a648ee7cf8823320ae7d7c8cd8c45)) 44 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using OpenFeature.Constant; 5 | using OpenFeature.Error; 6 | using OpenFeature.Model; 7 | 8 | namespace OpenFeature.Contrib.Providers.EnvVar; 9 | 10 | /// 11 | /// An OpenFeature provider using environment variables. 12 | /// 13 | public sealed class EnvVarProvider : FeatureProvider 14 | { 15 | private const string Name = "Environment Variable Provider"; 16 | private readonly string _prefix; 17 | private delegate bool TryConvert(string value, out TResult result); 18 | 19 | /// 20 | /// Creates a new instance of 21 | /// 22 | public EnvVarProvider() : this(string.Empty) 23 | { 24 | } 25 | 26 | /// 27 | /// Creates a new instance of 28 | /// 29 | /// A prefix which will be used when evaluating environment variables 30 | public EnvVarProvider(string prefix) 31 | { 32 | _prefix = prefix; 33 | } 34 | 35 | /// 36 | public override Metadata GetMetadata() 37 | { 38 | return new Metadata(Name); 39 | } 40 | 41 | private Task> Resolve(string flagKey, T defaultValue, TryConvert tryConvert) 42 | { 43 | var envVarName = $"{_prefix}{flagKey}"; 44 | var value = Environment.GetEnvironmentVariable(envVarName); 45 | 46 | if (value == null) 47 | return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, Reason.Error, string.Empty, $"Unable to find environment variable '{envVarName}'")); 48 | 49 | if (!tryConvert(value, out var convertedValue)) 50 | throw new FeatureProviderException(ErrorType.TypeMismatch, $"Could not convert the value of environment variable '{envVarName}' to {typeof(T)}"); 51 | 52 | return Task.FromResult(new ResolutionDetails(flagKey, convertedValue, ErrorType.None, Reason.Static)); 53 | } 54 | 55 | /// 56 | public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, 57 | CancellationToken cancellationToken = new CancellationToken()) 58 | { 59 | return Resolve(flagKey, defaultValue, bool.TryParse); 60 | } 61 | 62 | /// 63 | public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, 64 | CancellationToken cancellationToken = new CancellationToken()) 65 | { 66 | return Resolve(flagKey, defaultValue, NoopTryParse); 67 | 68 | bool NoopTryParse(string value, out string result) 69 | { 70 | result = value; 71 | return true; 72 | } 73 | } 74 | 75 | /// 76 | public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, 77 | CancellationToken cancellationToken = new CancellationToken()) 78 | { 79 | return Resolve(flagKey, defaultValue, int.TryParse); 80 | } 81 | 82 | /// 83 | public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, 84 | CancellationToken cancellationToken = new CancellationToken()) 85 | { 86 | return Resolve(flagKey, defaultValue, double.TryParse); 87 | } 88 | 89 | /// 90 | public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, 91 | CancellationToken cancellationToken = new CancellationToken()) 92 | { 93 | return Resolve(flagKey, defaultValue, ConvertStringToValue); 94 | 95 | bool ConvertStringToValue(string s, out Value value) 96 | { 97 | value = new Value(s); 98 | return true; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | OpenFeature.Contrib.Providers.EnvVar 5 | 0.0.7 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | Environment Variable Provider for .NET 10 | Octopus Deploy 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.EnvVar/README.md: -------------------------------------------------------------------------------- 1 | # .NET Environment Variable Provider 2 | 3 | The environment Variable provider allows you to read feature flags from the [process's environment](https://en.wikipedia.org/wiki/Environment_variable). 4 | 5 | ## Installation 6 | 7 | ### .NET CLI 8 | 9 | ```shell 10 | dotnet add package OpenFeature.Contrib.Providers.EnvVar 11 | ``` 12 | 13 | ## Using the Environment Variable Provider with the OpenFeature SDK 14 | 15 | The following example shows how to use the Environment Variable provider with the OpenFeature SDK. 16 | 17 | ```csharp 18 | using System; 19 | using OpenFeature; 20 | using OpenFeature.Contrib.EnvVar; 21 | 22 | // If you want to use a prefix for your environment variables, you can supply it in the constructor below. 23 | // For example, if you all your feature flag environment variables will be prefixed with feature-flag- then 24 | // you would use: 25 | // var envVarProvider = new EnvVarProvider("feature-flag-"); 26 | var envVarProvider = new EnvVarProvider(); 27 | 28 | // Set the Environment Variable provider as the provider for the OpenFeature SDK 29 | await OpenFeature.Api.Instance.SetProviderAsync(envVarProvider); 30 | var client = OpenFeature.Api.Instance.GetClient(); 31 | 32 | var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false); 33 | if (isAwesomeFeatureEnabled) 34 | { 35 | doTheNewThing(); 36 | } 37 | else 38 | { 39 | doTheOldThing(); 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.EnvVar/version.txt: -------------------------------------------------------------------------------- 1 | 0.0.7 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.FeatureManagement/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.FeatureManagement-v0.1.0...OpenFeature.Contrib.Providers.FeatureManagement-v0.1.1) (2025-05-30) 4 | 5 | 6 | ### ✨ New Features 7 | 8 | * Add support for basic flags in Feature Management ([#350](https://github.com/open-feature/dotnet-sdk-contrib/issues/350)) ([cfe5e57](https://github.com/open-feature/dotnet-sdk-contrib/commit/cfe5e5739edc0e812d3efcc01b740ecaad88d8a3)) 9 | 10 | 11 | ### 🧹 Chore 12 | 13 | * Add .editorconfig and cleanup code ([#375](https://github.com/open-feature/dotnet-sdk-contrib/issues/375)) ([683a392](https://github.com/open-feature/dotnet-sdk-contrib/commit/683a392604aca6c9a92b1f64fa30bc9e3e069b4f)) 14 | * **deps:** Upgrading to 4.0.0 of Microsoft.FeatureManagement ([#303](https://github.com/open-feature/dotnet-sdk-contrib/issues/303)) ([686cafc](https://github.com/open-feature/dotnet-sdk-contrib/commit/686cafc2c3b240736b61d0e32eec65b8449396c7)) 15 | * relax doc requirements for test projects ([e204e16](https://github.com/open-feature/dotnet-sdk-contrib/commit/e204e168ee8ccda34f46de325d45447e3ef85f73)) 16 | 17 | ## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.FeatureManagement-v0.0.4...OpenFeature.Contrib.Providers.FeatureManagement-v0.1.0) (2024-08-22) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) 23 | 24 | ### ✨ New Features 25 | 26 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) ([f845134](https://github.com/open-feature/dotnet-sdk-contrib/commit/f84513438586457087ac47fd40629912f2ec473a)) 27 | 28 | ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.FeatureManagement-v0.0.3...OpenFeature.Contrib.Providers.FeatureManagement-v0.0.4) (2024-08-21) 29 | 30 | 31 | ### 🐛 Bug Fixes 32 | 33 | * version expression ([cad2cd1](https://github.com/open-feature/dotnet-sdk-contrib/commit/cad2cd166d0c25753b37189f044c3a585cda0fad)) 34 | 35 | ## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.FeatureManagement-v0.0.2...OpenFeature.Contrib.Providers.FeatureManagement-v0.0.3) (2024-08-20) 36 | 37 | 38 | ### 🧹 Chore 39 | 40 | * update OpenFeature version compatiblity ([#249](https://github.com/open-feature/dotnet-sdk-contrib/issues/249)) ([232e948](https://github.com/open-feature/dotnet-sdk-contrib/commit/232e948a0916ca10612f85343e2eecebca107090)) 41 | 42 | ## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.FeatureManagement-v0.0.1...OpenFeature.Contrib.Providers.FeatureManagement-v0.0.2) (2024-07-23) 43 | 44 | 45 | ### ✨ New Features 46 | 47 | * Adding a Provider implementation on top of the standard dotnet FeatureManagement system. ([#129](https://github.com/open-feature/dotnet-sdk-contrib/issues/129)) ([69bf2d6](https://github.com/open-feature/dotnet-sdk-contrib/commit/69bf2d67606affa334792e5a9c70da9e4a28748e)) 48 | 49 | 50 | ### 🧹 Chore 51 | 52 | * add FeatureManagement documentation ([#197](https://github.com/open-feature/dotnet-sdk-contrib/issues/197)) ([01286c9](https://github.com/open-feature/dotnet-sdk-contrib/commit/01286c95228491707b2834fa2f2c4928c30800e4)) 53 | * add release config for FeatureManagement provider ([#135](https://github.com/open-feature/dotnet-sdk-contrib/issues/135)) ([60e03f8](https://github.com/open-feature/dotnet-sdk-contrib/commit/60e03f8417508e4d18c7943dabfe52634742f51f)) 54 | * Add support for GitHub Packages ([#134](https://github.com/open-feature/dotnet-sdk-contrib/issues/134)) ([0def0da](https://github.com/open-feature/dotnet-sdk-contrib/commit/0def0da173e2f327b7381eba043b6e99ae8f26fe)) 55 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenFeature.Contrib.Provider.FeatureManagement 5 | 0.1.1 6 | $(VersionNumber) 7 | preview 8 | $(VersionNumber) 9 | $(VersionNumber) 10 | An OpenFeature Provider built on top of the standard Microsoft FeatureManagement 11 | Library 12 | Eric Pattison 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.FeatureManagement/README.md: -------------------------------------------------------------------------------- 1 | # FeatureManagement .NET Provider 2 | > ![NOTE] 3 | > This requires a new feature of the FeatureManagement system, Variants. This feature is still in preview and has not been fully released. 4 | 5 | The FeatureManagement Provider allows you to use the FeatureManagement system as an OpenFeature Provider. 6 | 7 | ## Requirements 8 | 9 | - open-feature/dotnet-sdk v1.5.0 > v2.0.0 10 | 11 | ## .NET SDK Usage 12 | 13 | ### Install dependencies 14 | 15 | 16 | #### .NET Cli 17 | 18 | ```shell 19 | dotnet add package OpenFeature.Contrib.Provider.FeatureManagement --version 0.1.1 20 | ``` 21 | 22 | #### Package Manager 23 | 24 | ```shell 25 | NuGet\Install-Package OpenFeature.Contrib.Provider.FeatureManagement -Version 0.1.1 26 | ``` 27 | 28 | #### Package Reference 29 | 30 | ```xml 31 | 32 | ``` 33 | 34 | #### Paket CLI 35 | ```shell 36 | paket add OpenFeature.Contrib.Provider.FeatureManagement --version 0.1.1 37 | ``` 38 | 39 | #### Cake 40 | 41 | ```shell 42 | // Install OpenFeature.Contrib.Provider.FeatureManagement as a Cake Addin 43 | #addin nuget:?package=OpenFeature.Contrib.Provider.FeatureManagement&version=0.1.1&prerelease 44 | 45 | // Install OpenFeature.Contrib.Provider.FeatureManagement as a Cake Tool 46 | #tool nuget:?package=OpenFeature.Contrib.Provider.FeatureManagement&version=0.1.1&prerelease 47 | ``` 48 | 49 | 50 | ### Using the FeatureManagement Provider with the OpenFeature SDK 51 | 52 | FeatureManagement is built on top of .NETs Configuration system, so you must provide the loaded Configuration. 53 | Since Configuration is passed in any valid Configuration source can be used. 54 | For simplicity, we'll stick with a json file for all examples. 55 | 56 | ```csharp 57 | namespace OpenFeatureTestApp 58 | { 59 | class Program 60 | { 61 | static void Main(string[] args) 62 | { 63 | var configurationBuilder = new ConfigurationBuilder(); 64 | configurationBuilder.AddJsonFile("featureFlags.json"); 65 | 66 | IConfiguration configuration = configurationBuilder.Build(); 67 | 68 | var featureManagementProvider = new FeatureManagementProvider(configuration); 69 | OpenFeature.Api.Instance.SetProvider(featureManagementProvider); 70 | 71 | var client = OpenFeature.Api.Instance.GetClient(); 72 | 73 | var val = await client.GetBooleanValueAsync("myBoolFlag", false, null); 74 | 75 | System.Console.WriteLine(val.ToString()); 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | A simple example configuration would look like this. 82 | 83 | ```json 84 | { 85 | "FeatureManagement": { 86 | "myBoolFlag": { 87 | "Allocation": { 88 | "DefaultWhenEnabled": "FlagEnabled", 89 | "DefaultWhenDisabled": "FlagDisabled" 90 | }, 91 | "Variants": [ 92 | { 93 | "Name": "FlagEnabled", 94 | "ConfigurationValue": true 95 | }, 96 | { 97 | "Name": "FlagDisabled", 98 | "ConfigurationValue": false 99 | } 100 | ], 101 | "EnabledFor": [ 102 | { 103 | "Name": "AlwaysOn" 104 | } 105 | ] 106 | } 107 | } 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.FeatureManagement/version.txt: -------------------------------------------------------------------------------- 1 | 0.1.1 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Cache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 5 | 6 | namespace OpenFeature.Contrib.Providers.Flagd; 7 | 8 | internal interface ICache 9 | { 10 | void Add(TKey key, TValue value); 11 | TValue TryGet(TKey key); 12 | void Delete(TKey key); 13 | void Purge(); 14 | } 15 | class LRUCache : ICache where TValue : class 16 | { 17 | private readonly int _capacity; 18 | private readonly Dictionary _map; 19 | private Node _head; 20 | private Node _tail; 21 | 22 | private System.Threading.Mutex _mtx; 23 | 24 | public LRUCache(int capacity) 25 | { 26 | _capacity = capacity; 27 | _map = new Dictionary(); 28 | _mtx = new System.Threading.Mutex(); 29 | } 30 | 31 | public TValue TryGet(TKey key) 32 | { 33 | using (var mtx = new Mutex(ref _mtx)) 34 | { 35 | mtx.Lock(); 36 | if (_map.TryGetValue(key, out Node node)) 37 | { 38 | MoveToFront(node); 39 | return node.Value; 40 | } 41 | return default(TValue); 42 | } 43 | } 44 | 45 | public void Add(TKey key, TValue value) 46 | { 47 | using (var mtx = new Mutex(ref _mtx)) 48 | { 49 | mtx.Lock(); 50 | if (_map.TryGetValue(key, out Node node)) 51 | { 52 | node.Value = value; 53 | MoveToFront(node); 54 | } 55 | else 56 | { 57 | if (_map.Count >= _capacity) 58 | { 59 | _map.Remove(_tail.Key); 60 | RemoveTail(); 61 | } 62 | node = new Node(key, value); 63 | _map.Add(key, node); 64 | AddToFront(node); 65 | } 66 | } 67 | } 68 | 69 | public void Delete(TKey key) 70 | { 71 | using (var mtx = new Mutex(ref _mtx)) 72 | { 73 | mtx.Lock(); 74 | if (_map.TryGetValue(key, out Node node)) 75 | { 76 | if (node == _head) 77 | { 78 | _head = node.Next; 79 | } 80 | else 81 | { 82 | node.Prev.Next = node.Next; 83 | } 84 | if (node.Next != null) 85 | { 86 | node.Next.Prev = node.Prev; 87 | } 88 | _map.Remove(key); 89 | } 90 | } 91 | } 92 | 93 | public void Purge() 94 | { 95 | using (var mtx = new Mutex(ref _mtx)) 96 | { 97 | mtx.Lock(); 98 | _map.Clear(); 99 | } 100 | } 101 | 102 | private void MoveToFront(Node node) 103 | { 104 | if (node == _head) 105 | return; 106 | node.Prev.Next = node.Next; 107 | if (node == _tail) 108 | _tail = node.Prev; 109 | else 110 | node.Next.Prev = node.Prev; 111 | AddToFront(node); 112 | } 113 | 114 | private void AddToFront(Node node) 115 | { 116 | if (_head == null) 117 | { 118 | _head = node; 119 | _tail = node; 120 | return; 121 | } 122 | node.Next = _head; 123 | _head.Prev = node; 124 | _head = node; 125 | } 126 | 127 | private void RemoveTail() 128 | { 129 | _tail = _tail.Prev; 130 | if (_tail != null) 131 | _tail.Next = null; 132 | else 133 | _head = null; 134 | } 135 | 136 | private class Node 137 | { 138 | public TKey Key; 139 | public TValue Value; 140 | public Node Next; 141 | public Node Prev; 142 | 143 | public Node(TKey key, TValue value) 144 | { 145 | Key = key; 146 | Value = value; 147 | } 148 | } 149 | 150 | private class Mutex : System.IDisposable 151 | { 152 | 153 | public System.Threading.Mutex _mtx; 154 | 155 | public Mutex(ref System.Threading.Mutex mtx) 156 | { 157 | _mtx = mtx; 158 | } 159 | 160 | public void Lock() 161 | { 162 | _mtx.WaitOne(); 163 | } 164 | 165 | public void Dispose() 166 | { 167 | _mtx.ReleaseMutex(); 168 | } 169 | } 170 | } 171 | 172 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/DependencyInjection/FeatureBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Microsoft.Extensions.Options; 6 | using OpenFeature.Contrib.Providers.Flagd; 7 | using OpenFeature.Contrib.Providers.Flagd.DependencyInjection; 8 | 9 | namespace OpenFeature.DependencyInjection.Providers.Flagd; 10 | 11 | /// 12 | /// Extension methods for configuring the . 13 | /// 14 | public static class FeatureBuilderExtensions 15 | { 16 | /// 17 | /// Adds the to the with default configuration. 18 | /// 19 | /// The instance to configure. 20 | /// The instance for chaining. 21 | public static OpenFeatureBuilder AddFlagdProvider(this OpenFeatureBuilder builder) 22 | { 23 | builder.Services.AddOptions(FlagdProviderOptions.DefaultName); 24 | return builder.AddProvider(sp => CreateProvider(sp, null)); 25 | } 26 | 27 | /// 28 | /// Adds the to the with configuration. 29 | /// 30 | /// The instance to configure. 31 | /// Options to configure . 32 | /// The instance for chaining. 33 | public static OpenFeatureBuilder AddFlagdProvider(this OpenFeatureBuilder builder, Action options) 34 | { 35 | builder.Services.Configure(FlagdProviderOptions.DefaultName, options); 36 | return builder.AddProvider(sp => CreateProvider(sp, null)); 37 | } 38 | 39 | /// 40 | /// Adds the to the with a specific domain and default configuration. 41 | /// 42 | /// The instance to configure. 43 | /// The unique domain of the provider. 44 | /// The instance for chaining. 45 | public static OpenFeatureBuilder AddFlagdProvider(this OpenFeatureBuilder builder, string domain) 46 | { 47 | builder.Services.AddOptions(domain); 48 | return builder.AddProvider(domain, CreateProvider); 49 | } 50 | 51 | /// 52 | /// Adds the to the with a specific domain and configuration. 53 | /// 54 | /// The instance to configure. 55 | /// The unique domain of the provider. 56 | /// Options to configure . 57 | /// The instance for chaining. 58 | public static OpenFeatureBuilder AddFlagdProvider(this OpenFeatureBuilder builder, string domain, Action options) 59 | { 60 | builder.Services.Configure(domain, options); 61 | return builder.AddProvider(domain, CreateProvider); 62 | } 63 | 64 | private static FlagdProvider CreateProvider(IServiceProvider provider, string domain) 65 | { 66 | var optionsMonitor = provider.GetRequiredService>(); 67 | var logger = provider.GetService>(); 68 | logger ??= NullLogger.Instance; 69 | 70 | var options = string.IsNullOrEmpty(domain) 71 | ? optionsMonitor.Get(FlagdProviderOptions.DefaultName) 72 | : optionsMonitor.Get(domain); 73 | 74 | var config = options.ToFlagdConfig(); 75 | config.Logger = logger; 76 | 77 | return new FlagdProvider(config); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/DependencyInjection/FlagdProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Contrib.Providers.Flagd; 2 | 3 | namespace OpenFeature.DependencyInjection.Providers.Flagd; 4 | 5 | /// 6 | /// Configuration options for the Flagd provider. 7 | /// 8 | public record FlagdProviderOptions 9 | { 10 | /// 11 | /// Default name for the Flagd provider. 12 | /// 13 | public const string DefaultName = "FlagdProvider"; 14 | 15 | /// 16 | /// The host for the provider to connect to. Defaults to "localhost". 17 | /// 18 | public string Host { get; set; } = "localhost"; 19 | 20 | /// 21 | /// The Port property of the config. Defaults to 8013. 22 | /// 23 | public int Port { get; set; } = 8013; 24 | 25 | /// 26 | /// Use TLS for communication between the provider and the host. Defaults to false. 27 | /// 28 | public bool UseTls { get; set; } = false; 29 | 30 | /// 31 | /// Enable/disable the local cache for static flag values. Defaults to false. 32 | /// 33 | public bool CacheEnabled { get; set; } = false; 34 | 35 | /// 36 | /// The maximum size of the cache. Defaults to 10. 37 | /// 38 | public int MaxCacheSize { get; set; } = 10; 39 | 40 | /// 41 | /// Path to the certificate file. Defaults to empty string. 42 | /// 43 | public string CertificatePath { get; set; } = string.Empty; 44 | 45 | /// 46 | /// Path to the socket. Defaults to empty string. 47 | /// 48 | public string SocketPath { get; set; } = string.Empty; 49 | 50 | /// 51 | /// Maximum number of times the connection to the event stream should be re-attempted. Defaults to 3. 52 | /// 53 | public int MaxEventStreamRetries { get; set; } = 3; 54 | 55 | /// 56 | /// Which type of resolver to use. Defaults to . 57 | /// 58 | public ResolverType ResolverType { get; set; } = ResolverType.RPC; 59 | 60 | /// 61 | /// Source selector for the in-process provider. Defaults to empty string. 62 | /// 63 | public string SourceSelector { get; set; } = string.Empty; 64 | } 65 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/DependencyInjection/FlagdProviderOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.DependencyInjection.Providers.Flagd; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.DependencyInjection; 5 | 6 | internal static class FlagdProviderOptionsExtensions 7 | { 8 | public static FlagdConfig ToFlagdConfig(this FlagdProviderOptions options) 9 | { 10 | if (options == null) 11 | { 12 | throw new ArgumentNullException(nameof(options), "FlagdProviderOptions cannot be null."); 13 | } 14 | 15 | var config = FlagdConfig.Builder() 16 | .WithHost(options.Host) 17 | .WithPort(options.Port) 18 | .WithTls(options.UseTls) 19 | .WithCache(options.CacheEnabled) 20 | .WithMaxCacheSize(options.MaxCacheSize) 21 | .WithCertificatePath(options.CertificatePath) 22 | .WithSocketPath(options.SocketPath) 23 | .WithMaxEventStreamRetries(options.MaxEventStreamRetries) 24 | .WithResolverType(options.ResolverType) 25 | .WithSourceSelector(options.SourceSelector) 26 | .Build(); 27 | 28 | return config; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | OpenFeature.Contrib.Providers.Flagd 5 | 0.3.2 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | flagd provider for .NET 10 | Todd Baert 11 | true 12 | 13 | 14 | 15 | 16 | 17 | <_Parameter1>$(MSBuildProjectName).Test 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | runtime; build; native; contentfiles; analyzers; buildtransitive 37 | all 38 | 39 | 40 | 41 | 42 | 43 | 44 | [2.2,2.99999] 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using Json.Logic; 4 | 5 | namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; 6 | 7 | internal sealed class FlagdProperties 8 | { 9 | 10 | internal const string FlagdPropertiesKey = "$flagd"; 11 | internal const string FlagKeyKey = "flagKey"; 12 | internal const string TimestampKey = "timestamp"; 13 | internal const string TargetingKeyKey = "targetingKey"; 14 | 15 | internal string FlagKey { get; set; } 16 | internal long Timestamp { get; set; } 17 | internal string TargetingKey { get; set; } 18 | 19 | internal FlagdProperties(EvaluationContext from) 20 | { 21 | 22 | if (from.TryFind(TargetingKeyKey, out JsonNode targetingKeyValue) 23 | && targetingKeyValue.GetValueKind() == JsonValueKind.String) 24 | { 25 | TargetingKey = targetingKeyValue.ToString(); 26 | } 27 | if (from.TryFind($"{FlagdPropertiesKey}.{FlagKeyKey}", out JsonNode flagKeyValue) 28 | && flagKeyValue.GetValueKind() == JsonValueKind.String) 29 | { 30 | FlagKey = flagKeyValue.ToString(); 31 | } 32 | if (from.TryFind($"{FlagdPropertiesKey}.{TimestampKey}", out JsonNode timestampValue) 33 | && timestampValue.GetValueKind() == JsonValueKind.Number) 34 | { 35 | Timestamp = timestampValue.GetValue(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Text.Json.Nodes; 7 | using Json.Logic; 8 | using Murmur; 9 | 10 | namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; 11 | 12 | /// 13 | internal sealed class FractionalEvaluator : IRule 14 | { 15 | class FractionalEvaluationDistribution 16 | { 17 | public string variant; 18 | public int weight; 19 | } 20 | 21 | /// 22 | public JsonNode Apply(JsonNode args, EvaluationContext context) 23 | { 24 | // check if we have at least two arguments: 25 | // 1. the property value 26 | // 2. the array containing the buckets 27 | 28 | if (args.AsArray().Count == 0) 29 | { 30 | return null; 31 | } 32 | 33 | var flagdProperties = new FlagdProperties(context); 34 | 35 | var bucketStartIndex = 0; 36 | 37 | var arg0 = JsonLogic.Apply(args[0], context); 38 | 39 | string propertyValue; 40 | if (arg0.GetValueKind() == JsonValueKind.String) 41 | { 42 | propertyValue = arg0.ToString(); 43 | bucketStartIndex = 1; 44 | } 45 | else 46 | { 47 | propertyValue = flagdProperties.FlagKey + flagdProperties.TargetingKey; 48 | } 49 | 50 | var distributions = new List(); 51 | var distributionSum = 0; 52 | 53 | for (var i = bucketStartIndex; i < args.AsArray().Count; i++) 54 | { 55 | var bucket = JsonLogic.Apply(args[i], context); 56 | 57 | if (!(bucket.GetValueKind() == JsonValueKind.Array)) 58 | { 59 | continue; 60 | } 61 | 62 | var bucketArr = bucket.AsArray(); 63 | 64 | if (!bucketArr.Any()) 65 | { 66 | continue; 67 | } 68 | 69 | var weight = 1; 70 | 71 | if (bucketArr.Count >= 2 && bucketArr.ElementAt(1).GetValueKind() == JsonValueKind.Number) 72 | { 73 | weight = bucketArr.ElementAt(1).GetValue(); 74 | } 75 | 76 | distributions.Add(new FractionalEvaluationDistribution 77 | { 78 | variant = bucketArr.ElementAt(0).ToString(), 79 | weight = weight 80 | }); 81 | 82 | distributionSum += weight; 83 | } 84 | 85 | var valueToDistribute = propertyValue; 86 | var murmur32 = MurmurHash.Create32(); 87 | var bytes = Encoding.ASCII.GetBytes(valueToDistribute); 88 | var hashBytes = murmur32.ComputeHash(bytes); 89 | var hash = BitConverter.ToInt32(hashBytes, 0); 90 | 91 | var bucketValue = (int)(Math.Abs((float)hash) / Int32.MaxValue * 100); 92 | 93 | var rangeEnd = 0.0; 94 | 95 | foreach (var dist in distributions) 96 | { 97 | rangeEnd += 100 * (dist.weight / (float)distributionSum); 98 | if (bucketValue < rangeEnd) 99 | { 100 | return dist.variant; 101 | } 102 | } 103 | 104 | return ""; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using Json.Logic; 3 | using Semver; 4 | 5 | namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; 6 | 7 | /// 8 | internal sealed class SemVerRule : IRule 9 | { 10 | const string OperatorEqual = "="; 11 | const string OperatorNotEqual = "!="; 12 | const string OperatorLess = "<"; 13 | const string OperatorLessOrEqual = "<="; 14 | const string OperatorGreater = ">"; 15 | const string OperatorGreaterOrEqual = ">="; 16 | const string OperatorMatchMajor = "^"; 17 | const string OperatorMatchMinor = "~"; 18 | 19 | 20 | /// 21 | public JsonNode Apply(JsonNode args, EvaluationContext context) 22 | { 23 | // check if we have at least 3 arguments 24 | if (args.AsArray().Count < 3) 25 | { 26 | return false; 27 | } 28 | // get the value from the provided evaluation context 29 | var versionString = JsonLogic.Apply(args[0], context).ToString(); 30 | 31 | // get the operator 32 | var semVerOperator = JsonLogic.Apply(args[1], context).ToString(); 33 | 34 | // get the target version 35 | var targetVersionString = JsonLogic.Apply(args[2], context).ToString(); 36 | 37 | //convert to semantic versions 38 | if (!SemVersion.TryParse(versionString, SemVersionStyles.Strict, out var version) || 39 | !SemVersion.TryParse(targetVersionString, SemVersionStyles.Strict, out var targetVersion)) 40 | { 41 | return false; 42 | } 43 | 44 | switch (semVerOperator) 45 | { 46 | case OperatorEqual: 47 | return version.CompareSortOrderTo(targetVersion) == 0; 48 | case OperatorNotEqual: 49 | return version.CompareSortOrderTo(targetVersion) != 0; 50 | case OperatorLess: 51 | return version.CompareSortOrderTo(targetVersion) < 0; 52 | case OperatorLessOrEqual: 53 | return version.CompareSortOrderTo(targetVersion) <= 0; 54 | case OperatorGreater: 55 | return version.CompareSortOrderTo(targetVersion) > 0; 56 | case OperatorGreaterOrEqual: 57 | return version.CompareSortOrderTo(targetVersion) >= 0; 58 | case OperatorMatchMajor: 59 | return version.Major == targetVersion.Major; 60 | case OperatorMatchMinor: 61 | return version.Major == targetVersion.Major && version.Minor == targetVersion.Minor; 62 | default: 63 | return false; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | using Json.Logic; 5 | 6 | namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; 7 | 8 | internal sealed class StartsWithRule : IRule 9 | { 10 | public JsonNode Apply(JsonNode args, EvaluationContext context) 11 | { 12 | if (!StringRule.isValid(args, context, out string operandA, out string operandB)) 13 | { 14 | return false; 15 | } 16 | return Convert.ToString(operandA).StartsWith(Convert.ToString(operandB)); 17 | } 18 | } 19 | 20 | internal sealed class EndsWithRule : IRule 21 | { 22 | public JsonNode Apply(JsonNode args, EvaluationContext context) 23 | { 24 | if (!StringRule.isValid(args, context, out string operandA, out string operandB)) 25 | { 26 | return false; 27 | } 28 | return operandA.EndsWith(operandB); 29 | } 30 | } 31 | 32 | internal static class StringRule 33 | { 34 | internal static bool isValid(JsonNode args, EvaluationContext context, out string argA, out string argB) 35 | { 36 | argA = null; 37 | argB = null; 38 | 39 | // check if we have at least 2 arguments 40 | if (args.AsArray().Count < 2) 41 | { 42 | return false; 43 | } 44 | 45 | var nodeA = JsonLogic.Apply(args[0], context); 46 | var nodeB = JsonLogic.Apply(args[1], context); 47 | 48 | // return false immediately if both operands are not strings 49 | if (nodeA?.GetValueKind() != JsonValueKind.String || nodeB?.GetValueKind() != JsonValueKind.String) 50 | { 51 | return false; 52 | } 53 | 54 | argA = nodeA.ToString(); 55 | argB = nodeB.ToString(); 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonSchemaValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using NJsonSchema; 7 | using NJsonSchema.Generation; 8 | 9 | namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess; 10 | 11 | internal interface IJsonSchemaValidator 12 | { 13 | Task InitializeAsync(CancellationToken cancellationToken = default); 14 | void Validate(string configuration); 15 | } 16 | 17 | internal class JsonSchemaValidator : IJsonSchemaValidator 18 | { 19 | private readonly HttpClient _client; 20 | private readonly ILogger _logger; 21 | private JsonSchema _validator; 22 | 23 | internal JsonSchemaValidator(HttpClient client, ILogger logger) 24 | { 25 | if (client == null) 26 | { 27 | client = new HttpClient 28 | { 29 | BaseAddress = new Uri("https://flagd.dev"), 30 | }; 31 | } 32 | 33 | _client = client; 34 | _logger = logger; 35 | } 36 | 37 | public async Task InitializeAsync(CancellationToken cancellationToken = default) 38 | { 39 | try 40 | { 41 | var targetingTask = _client.GetAsync("/schema/v0/targeting.json", cancellationToken); 42 | var flagTask = _client.GetAsync("/schema/v0/flags.json", cancellationToken); 43 | 44 | await Task.WhenAll(targetingTask, flagTask).ConfigureAwait(false); 45 | 46 | var targeting = targetingTask.Result; 47 | var flag = flagTask.Result; 48 | 49 | if (!targeting.IsSuccessStatusCode) 50 | { 51 | _logger.LogWarning("Unable to retrieve Flagd targeting JSON Schema, status code: {StatusCode}", targeting.StatusCode); 52 | return; 53 | } 54 | 55 | if (!flag.IsSuccessStatusCode) 56 | { 57 | _logger.LogWarning("Unable to retrieve Flagd flags JSON Schema, status code: {StatusCode}", flag.StatusCode); 58 | return; 59 | } 60 | 61 | #if NET5_0_OR_GREATER 62 | var targetingJson = await targeting.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); 63 | #else 64 | var targetingJson = await targeting.Content.ReadAsStringAsync().ConfigureAwait(false); 65 | #endif 66 | 67 | var targetingSchema = await JsonSchema.FromJsonAsync(targetingJson, "targeting.json", schema => 68 | { 69 | var schemaResolver = new JsonSchemaResolver(schema, new SystemTextJsonSchemaGeneratorSettings()); 70 | var resolver = new JsonReferenceResolver(schemaResolver); 71 | 72 | return resolver; 73 | }, cancellationToken).ConfigureAwait(false); 74 | 75 | #if NET5_0_OR_GREATER 76 | var flagJson = await flag.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); 77 | #else 78 | var flagJson = await flag.Content.ReadAsStringAsync().ConfigureAwait(false); 79 | #endif 80 | var flagSchema = await JsonSchema.FromJsonAsync(flagJson, "flags.json", schema => 81 | { 82 | var schemaResolver = new JsonSchemaResolver(schema, new SystemTextJsonSchemaGeneratorSettings()); 83 | var resolver = new JsonReferenceResolver(schemaResolver); 84 | 85 | resolver.AddDocumentReference("targeting.json", targetingSchema); 86 | return resolver; 87 | }, cancellationToken).ConfigureAwait(false); 88 | 89 | _validator = flagSchema; 90 | } 91 | catch (Exception ex) 92 | { 93 | _logger.LogError(ex, "Unable to retrieve Flagd flags and targeting JSON Schemas"); 94 | } 95 | } 96 | 97 | public void Validate(string configuration) 98 | { 99 | if (_validator != null) 100 | { 101 | var errors = _validator.Validate(configuration); 102 | if (errors.Count > 0) 103 | { 104 | _logger.LogWarning("Validating Flagd configuration resulted in Schema Validation errors {Errors}", 105 | errors); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Resolver/Resolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using OpenFeature.Model; 3 | using Value = OpenFeature.Model.Value; 4 | 5 | namespace OpenFeature.Contrib.Providers.Flagd.Resolver; 6 | 7 | internal interface Resolver 8 | { 9 | Task Init(); 10 | Task Shutdown(); 11 | 12 | Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, 13 | EvaluationContext context = null); 14 | 15 | Task> ResolveStringValueAsync(string flagKey, string defaultValue, 16 | EvaluationContext context = null); 17 | 18 | Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, 19 | EvaluationContext context = null); 20 | 21 | Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, 22 | EvaluationContext context = null); 23 | 24 | Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, 25 | EvaluationContext context = null); 26 | } 27 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/UnixDomainSocketConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | #if NET5_0_OR_GREATER 2 | using System; 3 | using System.IO; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Sockets; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace OpenFeature.Contrib.Providers.Flagd; 11 | 12 | /// 13 | public class UnixDomainSocketConnectionFactory 14 | { 15 | private readonly EndPoint _endPoint; 16 | 17 | /// 18 | /// Constructor of the connection factory 19 | /// The path to the unix socket 20 | /// 21 | public UnixDomainSocketConnectionFactory(EndPoint endpoint) 22 | { 23 | _endPoint = endpoint; 24 | } 25 | 26 | #if NET5_0_OR_GREATER 27 | /// 28 | /// ConnectAsync is a custom implementation of the ConnectAsync method used by the grpc client 29 | /// 30 | /// unused - SocketsHttpConnectionContext 31 | /// The cancellation token 32 | /// A ValueTask object representing the given 33 | public async ValueTask ConnectAsync(SocketsHttpConnectionContext _, CancellationToken cancellationToken = default) 34 | { 35 | var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); 36 | 37 | try 38 | { 39 | await socket.ConnectAsync(_endPoint, cancellationToken).ConfigureAwait(false); 40 | return new NetworkStream(socket, true); 41 | } 42 | catch (Exception ex) 43 | { 44 | socket.Dispose(); 45 | throw new HttpRequestException($"Error connecting to '{_endPoint}'.", ex); 46 | } 47 | } 48 | #endif 49 | } 50 | #endif 51 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/Utils/CertificateLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography.X509Certificates; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.Utils; 5 | 6 | internal static class CertificateLoader 7 | { 8 | internal static X509Certificate2 LoadCertificate(string certificatePath) 9 | { 10 | if (string.IsNullOrWhiteSpace(certificatePath)) 11 | { 12 | return null; 13 | } 14 | 15 | if (!File.Exists(certificatePath)) 16 | { 17 | throw new FileNotFoundException($"Certificate file not found: {certificatePath}"); 18 | } 19 | 20 | #if NET9_0_OR_GREATER 21 | return X509CertificateLoader.LoadCertificateFromFile(certificatePath); 22 | #else 23 | return new X509Certificate2(certificatePath); 24 | #endif 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagd/version.txt: -------------------------------------------------------------------------------- 1 | 0.3.2 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProviderConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace OpenFeature.Contrib.Providers.Flagsmith; 2 | 3 | /// 4 | /// Settings for Flagsmith open feature provider 5 | /// 6 | public class FlagsmithProviderConfiguration : IFlagsmithProviderConfiguration 7 | { 8 | /// 9 | public bool UsingBooleanConfigValue { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagsmith/IFlagsmithProviderConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace OpenFeature.Contrib.Providers.Flagsmith; 2 | 3 | /// 4 | /// Settings for Flagsmith Open feature provider 5 | /// 6 | public interface IFlagsmithProviderConfiguration 7 | { 8 | /// 9 | /// Determines whether to resolve a feature value as a boolean or use 10 | /// the isFeatureEnabled as the flag itself. These values will be false 11 | /// and true respectively. 12 | /// Default: false 13 | /// 14 | public bool UsingBooleanConfigValue { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | OpenFeature.Contrib.Providers.Flagsmith 5 | 0.2.1 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | Flagsmith provider for .NET 10 | Vladimir Petrusevici 11 | true 12 | 13 | 14 | 15 | 16 | 17 | <_Parameter1>$(MSBuildProjectName).Test 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagsmith/README.md: -------------------------------------------------------------------------------- 1 | # Flagsmith .NET Provider 2 | 3 | The Flagsmith provider allows you to connect to your Flagsmith instance through the OpenFeature SDK 4 | 5 | # .Net SDK usage 6 | 7 | ## Requirements 8 | 9 | - open-feature/dotnet-sdk v1.5.0 > v2.0.0 10 | 11 | ## Install dependencies 12 | 13 | The first things we will do is install the **Open Feature SDK** and the **Flagsmith Feature Flag provider**. 14 | 15 | ### .NET Cli 16 | ```shell 17 | dotnet add package OpenFeature.Contrib.Providers.Flagsmith 18 | ``` 19 | ### Package Manager 20 | 21 | ```shell 22 | NuGet\Install-Package OpenFeature.Contrib.Providers.Flagsmith 23 | ``` 24 | ### Package Reference 25 | 26 | ```xml 27 | 28 | ``` 29 | ### Packet cli 30 | 31 | ```shell 32 | packet add OpenFeature.Contrib.Providers.Flagsmith 33 | ``` 34 | 35 | ### Cake 36 | 37 | ```shell 38 | // Install OpenFeature.Contrib.Providers.Flagsmith as a Cake Addin 39 | #addin nuget:?package=OpenFeature.Contrib.Providers.Flagsmith 40 | 41 | // Install OpenFeature.Contrib.Providers.Flagsmith as a Cake Tool 42 | #tool nuget:?package=OpenFeature.Contrib.Providers.Flagsmith 43 | ``` 44 | 45 | ## Using the Flagsmith Provider with the OpenFeature SDK 46 | 47 | To create a Flagmith provider you should define provider and Flagsmith settings. 48 | 49 | ```csharp 50 | using Flagsmith; 51 | using OpenFeature.Contrib.Providers.Flagsmith; 52 | using OpenFeature.Model; 53 | 54 | // Additional configs for provider 55 | var providerConfig = new FlagsmithProviderConfiguration(); 56 | 57 | // Flagsmith client configuration 58 | var flagsmithConfig = new FlagsmithConfiguration 59 | { 60 | ApiUrl = "https://edge.api.flagsmith.com/api/v1/", 61 | EnvironmentKey = "", 62 | EnableClientSideEvaluation = false, 63 | EnvironmentRefreshIntervalSeconds = 60, 64 | EnableAnalytics = false, 65 | Retries = 1, 66 | }; 67 | var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithConfig); 68 | 69 | // Set the flagsmithProvider as the provider for the OpenFeature SDK 70 | await OpenFeature.Api.Instance.SetProviderAsync(flagsmithProvider); 71 | 72 | // Get an OpenFeature client 73 | var client = OpenFeature.Api.Instance.GetClient("my-app"); 74 | 75 | // Optional: set a targeting key and traits to use segment and/or identity overrides 76 | var context = EvaluationContext.Builder() 77 | .SetTargetingKey("my-flagsmith-identity-ID") 78 | .Set("my-trait-key", "my-trait-value") 79 | .Build(); 80 | 81 | // Evaluate a flag 82 | var val = await client.GetBooleanValueAsync("myBoolFlag", false, context); 83 | 84 | // Print the value of the 'myBoolFlag' feature flag 85 | Console.WriteLine(val); 86 | ``` 87 | 88 | You also can create Flagsmith provider using ```HttpClient``` or precreated ```FlagsmithClient``` 89 | 90 | ```csharp 91 | using var httpClient = new HttpClient(); 92 | var flagsmithProvider = new FlagsmithProvider(providerConfig, config, httpClient); 93 | ``` 94 | ```csharp 95 | using var flagsmithClient = new FlagsmithClient(flagsmithOptions); 96 | var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); 97 | ``` 98 | ### Configuring the FlagsmithProvider 99 | 100 | To configure FlagsmithConfiguration just use [an example](https://github.com/Flagsmith/flagsmith-dotnet-client/tree/main/Example) from Flagsmith GitHub. 101 | For FlagsmithProviderConfiguration you can configure next parameters using custom implementation or just ```FlagsmithProviderConfiguration```: 102 | ```csharp 103 | public interface IFlagsmithProviderConfiguration 104 | { 105 | /// 106 | /// Key that will be used as identity for Flagsmith requests. 107 | /// 108 | public string TargetingKey { get; } 109 | 110 | /// 111 | /// Determines whether to resolve a feature value as a boolean or use 112 | /// the isFeatureEnabled as the flag itself. These values will be false 113 | /// and true respectively. 114 | /// Default: false 115 | /// 116 | public bool UsingBooleanConfigValue { get; } 117 | } 118 | ``` 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flagsmith/version.txt: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.4...OpenFeature.Contrib.Providers.Flipt-v0.0.5) (2024-10-18) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * update readme ([1aaa387](https://github.com/open-feature/dotnet-sdk-contrib/commit/1aaa3877ae3db884d401226b2138f8e3903a56c2)) 9 | 10 | ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.3...OpenFeature.Contrib.Providers.Flipt-v0.0.4) (2024-10-18) 11 | 12 | 13 | ### 🐛 Bug Fixes 14 | 15 | * update docs ([#300](https://github.com/open-feature/dotnet-sdk-contrib/issues/300)) ([50fd738](https://github.com/open-feature/dotnet-sdk-contrib/commit/50fd738585567a39f6fd0b1db37b899cbae42ba5)) 16 | 17 | ## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.2...OpenFeature.Contrib.Providers.Flipt-v0.0.3) (2024-10-18) 18 | 19 | 20 | ### 🐛 Bug Fixes 21 | 22 | * force a republish ([#298](https://github.com/open-feature/dotnet-sdk-contrib/issues/298)) ([ad01db2](https://github.com/open-feature/dotnet-sdk-contrib/commit/ad01db2991a147d527637afac30827f73a4cc40e)) 23 | 24 | ## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.1...OpenFeature.Contrib.Providers.Flipt-v0.0.2) (2024-10-17) 25 | 26 | 27 | ### ✨ New Features 28 | 29 | * Introduce flipt provider for dotnet ([#293](https://github.com/open-feature/dotnet-sdk-contrib/issues/293)) ([4d59bc3](https://github.com/open-feature/dotnet-sdk-contrib/commit/4d59bc35bd4c65c9989e8c980668d85242240eec)) 30 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Flipt.Rest; 5 | 6 | namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper; 7 | 8 | /// 9 | /// Wrapper for Flipt server sdk client for .net 10 | /// 11 | public class FliptClientWrapper : IFliptClientWrapper 12 | { 13 | private readonly FliptRestClient _fliptRestClient; 14 | 15 | /// 16 | /// 17 | /// Url of flipt instance 18 | /// Authentication access token 19 | /// Timeout when calling flipt endpoints in seconds 20 | public FliptClientWrapper(string fliptUrl, 21 | string clientToken = "", 22 | int timeoutInSeconds = 30) 23 | { 24 | _fliptRestClient = BuildClient(fliptUrl, clientToken, timeoutInSeconds); 25 | } 26 | 27 | /// 28 | public async Task EvaluateVariantAsync(EvaluationRequest evaluationRequest) 29 | { 30 | return await _fliptRestClient.EvaluateV1VariantAsync(evaluationRequest).ConfigureAwait(false); 31 | } 32 | 33 | /// 34 | public async Task EvaluateBooleanAsync(EvaluationRequest evaluationRequest) 35 | { 36 | return await _fliptRestClient.EvaluateV1BooleanAsync(evaluationRequest).ConfigureAwait(false); 37 | } 38 | 39 | private static FliptRestClient BuildClient(string fliptUrl, string clientToken, int timeoutInSeconds = 30) 40 | { 41 | var httpClient = new HttpClient 42 | { 43 | BaseAddress = new Uri(fliptUrl), 44 | Timeout = TimeSpan.FromSeconds(timeoutInSeconds), 45 | DefaultRequestHeaders = { { "Authorization", $"Bearer {clientToken}" } } 46 | }; 47 | return new FliptRestClient(httpClient); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Flipt.Rest; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper; 5 | 6 | /// 7 | /// 8 | public interface IFliptClientWrapper 9 | { 10 | /// 11 | /// Wrapper to Flipt.io/EvaluateVariantAsync method 12 | /// 13 | /// 14 | /// 15 | Task EvaluateVariantAsync(EvaluationRequest evaluationRequest); 16 | 17 | /// 18 | /// Wrapper to Flipt.io/EvaluateBooleanAsync method 19 | /// 20 | /// 21 | /// 22 | Task EvaluateBooleanAsync(EvaluationRequest evaluationRequest); 23 | } 24 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace OpenFeature.Contrib.Providers.Flipt.Converters; 4 | 5 | /// 6 | /// Extensions for default JsonConverter behavior 7 | /// 8 | public static class JsonConverterExtensions 9 | { 10 | /// 11 | /// JsonConverter serializer settings for Flipt to OpenFeature model deserialization 12 | /// 13 | public static readonly JsonSerializerOptions DefaultSerializerSettings = new() 14 | { 15 | WriteIndented = true, 16 | AllowTrailingCommas = true, 17 | Converters = 18 | { 19 | new OpenFeatureStructureConverter(), 20 | new OpenFeatureValueConverter() 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.Flipt.Converters; 8 | 9 | /// 10 | /// JsonConverter for OpenFeature Structure type 11 | /// 12 | public class OpenFeatureStructureConverter : JsonConverter 13 | { 14 | /// 15 | public override void Write(Utf8JsonWriter writer, Structure value, JsonSerializerOptions options) 16 | { 17 | var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsDictionary(), 18 | JsonConverterExtensions.DefaultSerializerSettings)); 19 | jsonDoc.WriteTo(writer); 20 | } 21 | 22 | /// 23 | public override Structure Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 24 | { 25 | using var jsonDocument = JsonDocument.ParseValue(ref reader); 26 | var jsonText = jsonDocument.RootElement.GetRawText(); 27 | return new Structure(JsonSerializer.Deserialize>(jsonText, 28 | JsonConverterExtensions.DefaultSerializerSettings)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.Flipt.Converters; 8 | 9 | /// 10 | /// OpenFeature Value type converter 11 | /// 12 | public class OpenFeatureValueConverter : JsonConverter 13 | { 14 | /// 15 | public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 16 | { 17 | var value = new Value(); 18 | switch (reader.TokenType) 19 | { 20 | case JsonTokenType.String: 21 | return reader.TryGetDateTime(out var dateTimeValue) 22 | ? new Value(dateTimeValue) 23 | : new Value(reader.GetString() ?? string.Empty); 24 | case JsonTokenType.True: 25 | case JsonTokenType.False: 26 | return new Value(reader.GetBoolean()); 27 | case JsonTokenType.Number: 28 | if (reader.TryGetInt32(out var intValue)) return new Value(intValue); 29 | if (reader.TryGetDouble(out var dblValue)) return new Value(dblValue); 30 | break; 31 | case JsonTokenType.StartArray: 32 | return new Value(GenerateValueArray(ref reader, typeToConvert, options)); 33 | case JsonTokenType.StartObject: 34 | return new Value(GetStructure(ref reader, typeToConvert, options)); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | private Structure GetStructure(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 41 | { 42 | var startDepth = reader.CurrentDepth; 43 | var structureDictionary = new Dictionary(); 44 | while (reader.Read()) 45 | { 46 | if (reader.TokenType == JsonTokenType.PropertyName) 47 | { 48 | var key = reader.GetString(); 49 | reader.Read(); 50 | var val = Read(ref reader, typeToConvert, options); 51 | structureDictionary[key ?? string.Empty] = val; 52 | } 53 | 54 | if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) break; 55 | } 56 | 57 | return new Structure(structureDictionary); 58 | } 59 | 60 | 61 | private IList GenerateValueArray(ref Utf8JsonReader reader, Type typeToConvert, 62 | JsonSerializerOptions options) 63 | { 64 | var valuesArray = new List(); 65 | var startDepth = reader.CurrentDepth; 66 | 67 | while (reader.Read()) 68 | switch (reader.TokenType) 69 | { 70 | case JsonTokenType.EndArray when reader.CurrentDepth == startDepth: 71 | return valuesArray; 72 | default: 73 | valuesArray.Add(Read(ref reader, typeToConvert, options)); 74 | break; 75 | } 76 | 77 | return valuesArray; 78 | } 79 | 80 | /// 81 | public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) 82 | { 83 | if (value.IsList) 84 | { 85 | writer.WriteStartArray(); 86 | foreach (var val in value.AsList!) 87 | { 88 | var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(val.AsObject, 89 | JsonConverterExtensions.DefaultSerializerSettings)); 90 | jsonDoc.WriteTo(writer); 91 | } 92 | 93 | writer.WriteEndArray(); 94 | } 95 | else 96 | { 97 | var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsObject, 98 | JsonConverterExtensions.DefaultSerializerSettings)); 99 | jsonDoc.WriteTo(writer); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.Json; 4 | using OpenFeature.Contrib.Providers.Flipt.Converters; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.Flipt; 8 | 9 | /// 10 | /// Extension helper methods 11 | /// 12 | public static class FliptExtensions 13 | { 14 | /// 15 | /// Transforms openFeature EvaluationContext to a mutable Dictionary that flipt sdk accepts 16 | /// 17 | /// OpenFeature EvaluationContext 18 | /// 19 | public static Dictionary ToStringDictionary(this EvaluationContext evaluationContext) 20 | { 21 | return evaluationContext?.AsDictionary() 22 | .ToDictionary(k => k.Key, 23 | v => JsonSerializer.Serialize(v.Value.AsObject, 24 | JsonConverterExtensions.DefaultSerializerSettings)) ?? 25 | []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using OpenFeature.Model; 4 | 5 | namespace OpenFeature.Contrib.Providers.Flipt; 6 | 7 | /// 8 | /// FliptProvider is the .NET provider implementation for Flipt.io 9 | /// 10 | /// 11 | /// Accepts an instantiated IFliptClientWrapper instance 12 | /// 13 | public class FliptProvider : FeatureProvider 14 | { 15 | private static readonly Metadata Metadata = new("Flipt Provider"); 16 | private readonly IFliptToOpenFeatureConverter _fliptToOpenFeatureConverter; 17 | 18 | /// 19 | /// Instantiate a FliptProvider using configuration params 20 | /// 21 | /// Url of flipt instance 22 | /// Namespace used for querying flags 23 | /// Authentication access token 24 | /// Timeout when calling flipt endpoints in seconds 25 | public FliptProvider(string fliptUrl, string namespaceKey = "default", string clientToken = "", 26 | int timeoutInSeconds = 30) : this(new FliptToOpenFeatureConverter(fliptUrl, namespaceKey, clientToken, 27 | timeoutInSeconds)) 28 | { 29 | } 30 | 31 | internal FliptProvider(IFliptToOpenFeatureConverter fliptToOpenFeatureConverter) 32 | { 33 | _fliptToOpenFeatureConverter = fliptToOpenFeatureConverter; 34 | } 35 | 36 | /// 37 | public override Metadata GetMetadata() 38 | { 39 | return Metadata; 40 | } 41 | 42 | /// 43 | public override async Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, 44 | EvaluationContext context = null, 45 | CancellationToken cancellationToken = new()) 46 | { 47 | return await _fliptToOpenFeatureConverter.EvaluateBooleanAsync(flagKey, defaultValue, context).ConfigureAwait(false); 48 | } 49 | 50 | /// 51 | public override async Task> ResolveStringValueAsync(string flagKey, 52 | string defaultValue, EvaluationContext context = null, 53 | CancellationToken cancellationToken = new()) 54 | { 55 | return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context).ConfigureAwait(false); 56 | } 57 | 58 | /// 59 | public override async Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, 60 | EvaluationContext context = null, 61 | CancellationToken cancellationToken = new()) 62 | { 63 | return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context).ConfigureAwait(false); 64 | } 65 | 66 | /// 67 | public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, 68 | EvaluationContext context = null, 69 | CancellationToken cancellationToken = new()) 70 | { 71 | return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context).ConfigureAwait(false); 72 | } 73 | 74 | /// 75 | public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, 76 | EvaluationContext context = null, 77 | CancellationToken cancellationToken = new()) 78 | { 79 | return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context).ConfigureAwait(false); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | OpenFeature.Contrib.Providers.Flipt 5 | 0.0.5 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | Flipt provider for .NET 10 | Jean Andrei de la Cruz Austria 11 | true 12 | 13 | 14 | 15 | 16 | 17 | <_Parameter1>$(MSBuildProjectName).Test 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Flipt/version.txt: -------------------------------------------------------------------------------- 1 | 0.0.5 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using OpenFeature.Contrib.Providers.GOFeatureFlag.models; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag; 6 | 7 | /// 8 | /// GoFeatureFlagProviderOptions contains the options to initialise the provider. 9 | /// 10 | public class GoFeatureFlagProviderOptions 11 | { 12 | /// 13 | /// (mandatory) endpoint contains the DNS of your GO Feature Flag relay proxy 14 | /// example: https://mydomain.com/gofeatureflagproxy/ 15 | /// 16 | public string Endpoint { get; set; } 17 | 18 | /// 19 | /// (optional) timeout we are waiting when calling the go-feature-flag relay proxy API. 20 | /// Default: 10000 ms 21 | /// 22 | public TimeSpan Timeout { get; set; } = new TimeSpan(10000 * TimeSpan.TicksPerMillisecond); 23 | 24 | /// 25 | /// (optional) If you want to provide your own HttpMessageHandler. 26 | /// Default: null 27 | /// 28 | public HttpMessageHandler HttpMessageHandler { get; set; } 29 | 30 | /// 31 | /// (optional) If the relay proxy is configured to authenticate the request, you should provide 32 | /// an API Key to the provider. 33 | /// Please ask the administrator of the relay proxy to provide an API Key. 34 | /// (This feature is available only if you are using GO Feature Flag relay proxy v1.7.0 or above) 35 | /// Default: null 36 | /// 37 | public string ApiKey { get; set; } 38 | 39 | /// 40 | /// (optional) ExporterMetadata are static information you can set that will be available in the 41 | /// evaluation data sent to the exporter. 42 | /// 43 | public ExporterMetadata ExporterMetadata { get; set; } 44 | } 45 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenFeature.Contrib.GOFeatureFlag 5 | 0.2.1 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | GO Feature Flag provider for .NET 10 | Thomas Poignant 11 | true 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/README.md: -------------------------------------------------------------------------------- 1 | # GO Feature Flag .NET Provider 2 | 3 | GO Feature Flag provider allows you to connect to your GO Feature Flag instance. 4 | 5 | [GO Feature Flag](https://gofeatureflag.org) believes in simplicity and offers a simple and lightweight solution to use feature flags. 6 | Our focus is to avoid any complex infrastructure work to use GO Feature Flag. 7 | 8 | This is a complete feature flagging solution with the possibility to target only a group of users, use any types of flags, store your configuration in various location and advanced rollout functionality. You can also collect usage data of your flags and be notified of configuration changes. 9 | 10 | # .Net SDK usage 11 | 12 | ## Requirements 13 | 14 | - open-feature/dotnet-sdk v1.5.0 > v2.0.0 15 | 16 | ## Install dependencies 17 | 18 | The first things we will do is install the **Open Feature SDK** and the **GO Feature Flag provider**. 19 | 20 | ### .NET Cli 21 | ```shell 22 | dotnet add package OpenFeature.Contrib.GOFeatureFlag 23 | ``` 24 | ### Package Manager 25 | 26 | ```shell 27 | NuGet\Install-Package OpenFeature.Contrib.GOFeatureFlag 28 | ``` 29 | ### Package Reference 30 | 31 | ```xml 32 | 33 | ``` 34 | ### Packet cli 35 | 36 | ```shell 37 | paket add OpenFeature.Contrib.GOFeatureFlag 38 | ``` 39 | 40 | ### Cake 41 | 42 | ```shell 43 | // Install OpenFeature.Contrib.GOFeatureFlag as a Cake Addin 44 | #addin nuget:?package=OpenFeature.Contrib.GOFeatureFlag 45 | 46 | // Install OpenFeature.Contrib.GOFeatureFlag as a Cake Tool 47 | #tool nuget:?package=OpenFeature.Contrib.GOFeatureFlag 48 | ``` 49 | 50 | ## Initialize your Open Feature client 51 | 52 | To evaluate the flags you need to have an Open Feature configured in you app. 53 | This code block shows you how you can create a client that you can use in your application. 54 | 55 | ```csharp 56 | using OpenFeature; 57 | using OpenFeature.Contrib.Providers.GOFeatureFlag; 58 | 59 | // ... 60 | 61 | var goFeatureFlagProvider = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions 62 | { 63 | Endpoint = "http://localhost:1031/", 64 | Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) 65 | }); 66 | Api.Instance.SetProvider(goFeatureFlagProvider); 67 | var client = Api.Instance.GetClient("my-app"); 68 | ``` 69 | 70 | ## Evaluate your flag 71 | 72 | This code block explain how you can create an `EvaluationContext` and use it to evaluate your flag. 73 | 74 | 75 | > In this example we are evaluating a `boolean` flag, but other types are available. 76 | > 77 | > **Refer to the [Open Feature documentation](https://openfeature.dev/docs/reference/concepts/evaluation-api#basic-evaluation) to know more about it.** 78 | 79 | ```csharp 80 | // Context of your flag evaluation. 81 | // With GO Feature Flag you MUST have a targetingKey that is a unique identifier of the user. 82 | var userContext = EvaluationContext.Builder() 83 | .Set("targetingKey", "1d1b9238-2591-4a47-94cf-d2bc080892f1") // user unique identifier (mandatory) 84 | .Set("firstname", "john") 85 | .Set("lastname", "doe") 86 | .Set("email", "john.doe@gofeatureflag.org") 87 | .Set("admin", true) // this field is used in the targeting rule of the flag "flag-only-for-admin" 88 | .Set("anonymous", false) 89 | .Build(); 90 | 91 | var adminFlag = await client.GetBooleanValueAsync("flag-only-for-admin", false, userContext); 92 | if (adminFlag) { 93 | // flag "flag-only-for-admin" is true for the user 94 | } else { 95 | // flag "flag-only-for-admin" is false for the user 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.Json; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters; 6 | 7 | /// 8 | /// DictionaryConverter is converting a json Dictionary to a Dictionary with real object. 9 | /// 10 | public static class DictionaryConverter 11 | { 12 | /// 13 | /// Function that convert the dictionary to a Dictionary with real object. 14 | /// 15 | /// 16 | /// A dictionary with real types. 17 | public static Dictionary ConvertDictionary(Dictionary inputDictionary) 18 | { 19 | return inputDictionary.ToDictionary( 20 | kvp => kvp.Key, 21 | kvp => ConvertValue(kvp.Value) 22 | ); 23 | } 24 | 25 | /// 26 | /// Function that convert a value to a object. 27 | /// 28 | /// 29 | /// A value with real types. 30 | public static object ConvertValue(object value) 31 | { 32 | if (value is JsonElement jsonElement) 33 | switch (jsonElement.ValueKind) 34 | { 35 | case JsonValueKind.String: 36 | return jsonElement.GetString(); 37 | case JsonValueKind.Number: 38 | if (jsonElement.TryGetInt32(out var intValue)) return intValue; 39 | 40 | if (jsonElement.TryGetDouble(out var doubleValue)) return doubleValue; 41 | return jsonElement.GetRawText(); // Fallback to string if not int or double 42 | case JsonValueKind.True: 43 | return true; 44 | case JsonValueKind.False: 45 | return false; 46 | case JsonValueKind.Null: 47 | return null; 48 | case JsonValueKind.Object: 49 | return ConvertDictionary( 50 | JsonSerializer 51 | .Deserialize>(jsonElement 52 | .GetRawText())); //Recursive for nested objects 53 | case JsonValueKind.Array: 54 | var array = new List(); 55 | foreach (var element in jsonElement.EnumerateArray()) array.Add(ConvertValue(element)); 56 | 57 | return array; 58 | default: 59 | return jsonElement.GetRawText(); // Handle other types as needed 60 | } 61 | 62 | return value; // Return original value if not a JsonElement 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/JsonConverterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters; 4 | 5 | /// 6 | /// Extensions for default JsonConverter behavior 7 | /// 8 | public static class JsonConverterExtensions 9 | { 10 | /// 11 | /// JsonConverter serializer settings for GO Feature Flag to OpenFeature model deserialization 12 | /// 13 | public static readonly JsonSerializerOptions DefaultSerializerSettings = new JsonSerializerOptions 14 | { 15 | WriteIndented = true, 16 | AllowTrailingCommas = true, 17 | Converters = 18 | { 19 | new OpenFeatureStructureConverter(), 20 | new OpenFeatureValueConverter() 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureStructureConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters; 8 | 9 | /// 10 | /// OpenFeatureStructureConverter 11 | /// 12 | public class OpenFeatureStructureConverter : JsonConverter 13 | { 14 | /// 15 | public override void Write(Utf8JsonWriter writer, Structure value, JsonSerializerOptions options) 16 | { 17 | var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsDictionary(), 18 | JsonConverterExtensions.DefaultSerializerSettings)); 19 | jsonDoc.WriteTo(writer); 20 | } 21 | 22 | /// 23 | public override Structure Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 24 | { 25 | using var jsonDocument = JsonDocument.ParseValue(ref reader); 26 | var jsonText = jsonDocument.RootElement.GetRawText(); 27 | return new Structure(JsonSerializer.Deserialize>(jsonText, 28 | JsonConverterExtensions.DefaultSerializerSettings)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters; 8 | 9 | /// 10 | /// OpenFeature Value type converter 11 | /// 12 | public class OpenFeatureValueConverter : JsonConverter 13 | { 14 | /// 15 | public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 16 | { 17 | var value = new Value(); 18 | switch (reader.TokenType) 19 | { 20 | case JsonTokenType.String: 21 | return reader.TryGetDateTime(out var dateTimeValue) 22 | ? new Value(dateTimeValue) 23 | : new Value(reader.GetString() ?? string.Empty); 24 | case JsonTokenType.True: 25 | case JsonTokenType.False: 26 | return new Value(reader.GetBoolean()); 27 | case JsonTokenType.Number: 28 | if (reader.TryGetInt32(out var intValue)) return new Value(intValue); 29 | if (reader.TryGetDouble(out var dblValue)) return new Value(dblValue); 30 | break; 31 | case JsonTokenType.StartArray: 32 | return new Value(GenerateValueArray(ref reader, typeToConvert, options)); 33 | case JsonTokenType.StartObject: 34 | return new Value(GetStructure(ref reader, typeToConvert, options)); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | private Structure GetStructure(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 41 | { 42 | var startDepth = reader.CurrentDepth; 43 | var structureDictionary = new Dictionary(); 44 | while (reader.Read()) 45 | { 46 | if (reader.TokenType == JsonTokenType.PropertyName) 47 | { 48 | var key = reader.GetString(); 49 | reader.Read(); 50 | var val = Read(ref reader, typeToConvert, options); 51 | structureDictionary[key ?? string.Empty] = val; 52 | } 53 | 54 | if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) break; 55 | } 56 | 57 | return new Structure(structureDictionary); 58 | } 59 | 60 | 61 | private IList GenerateValueArray(ref Utf8JsonReader reader, Type typeToConvert, 62 | JsonSerializerOptions options) 63 | { 64 | var valuesArray = new List(); 65 | var startDepth = reader.CurrentDepth; 66 | 67 | while (reader.Read()) 68 | switch (reader.TokenType) 69 | { 70 | case JsonTokenType.EndArray when reader.CurrentDepth == startDepth: 71 | return valuesArray; 72 | default: 73 | valuesArray.Add(Read(ref reader, typeToConvert, options)); 74 | break; 75 | } 76 | 77 | return valuesArray; 78 | } 79 | 80 | /// 81 | public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) 82 | { 83 | if (value.IsList) 84 | { 85 | writer.WriteStartArray(); 86 | foreach (var val in value.AsList!) 87 | { 88 | var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(val.AsObject, 89 | JsonConverterExtensions.DefaultSerializerSettings)); 90 | jsonDoc.WriteTo(writer); 91 | } 92 | 93 | writer.WriteEndArray(); 94 | } 95 | else 96 | { 97 | var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsObject, 98 | JsonConverterExtensions.DefaultSerializerSettings)); 99 | jsonDoc.WriteTo(writer); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagDisabled.cs: -------------------------------------------------------------------------------- 1 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 2 | 3 | /// 4 | /// Exception thrown when a flag is disabled 5 | /// 6 | public class FlagDisabled : GoFeatureFlagException 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagNotFoundError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception thrown when the flag is not found by GO Feature Flag relay proxy. 9 | /// 10 | public class FlagNotFoundError : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public FlagNotFoundError(string message, Exception innerException = null) : base(ErrorType.FlagNotFound, 18 | message, innerException) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GeneralError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception throw when we don't have a specific case. 9 | /// 10 | public class GeneralError : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public GeneralError(string message, Exception innerException = null) : base(ErrorType.General, message, 18 | innerException) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GoFeatureFlagException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 4 | 5 | /// 6 | /// GoFeatureFlagException is the root exception of GO Feature Flag provider. 7 | /// 8 | public abstract class GoFeatureFlagException : Exception 9 | { 10 | /// 11 | /// Constructor 12 | /// 13 | public GoFeatureFlagException() 14 | { 15 | } 16 | 17 | /// 18 | /// Constructor 19 | /// 20 | /// Message of your exception 21 | public GoFeatureFlagException(string message) 22 | : base(message) 23 | { 24 | } 25 | 26 | /// 27 | /// Constructor 28 | /// 29 | /// Message of your exception 30 | /// Root exception. 31 | public GoFeatureFlagException(string message, Exception inner) 32 | : base(message, inner) 33 | { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/ImpossibleToConvertTypeError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception throw when we have a type that we are not able to convert. 9 | /// 10 | public class ImpossibleToConvertTypeError : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public ImpossibleToConvertTypeError(string message, Exception innerException = null) : base( 18 | ErrorType.ParseError, 19 | message, innerException) 20 | { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidEvaluationContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception throw when the Evaluation Context is invalid. 9 | /// 10 | public class InvalidEvaluationContext : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public InvalidEvaluationContext(string message, Exception innerException = null) : base( 18 | ErrorType.InvalidContext, message, innerException) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidOption.cs: -------------------------------------------------------------------------------- 1 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 2 | 3 | /// 4 | /// Exception throw when the options of the provider are invalid. 5 | /// 6 | public class InvalidOption : GoFeatureFlagException 7 | { 8 | /// 9 | /// Constructor of the exception 10 | /// 11 | /// Message to display 12 | public InvalidOption(string message) : base(message) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidTargetingKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception throw when The Evaluation Context does not contains a targetingKey field. 9 | /// 10 | public class InvalidTargetingKey : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public InvalidTargetingKey(string message, Exception innerException = null) : base(ErrorType.InvalidContext, 18 | message, innerException) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/TypeMismatchError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception throw when the type we received from GO Feature Flag is different than the one expected. 9 | /// 10 | public class TypeMismatchError : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public TypeMismatchError(string message, Exception innerException = null) : base(ErrorType.TypeMismatch, 18 | message, innerException) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/UnauthorizedError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Error; 4 | 5 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 6 | 7 | /// 8 | /// Exception throw when we are not authorized to call the API in the relay proxy. 9 | /// 10 | public class UnauthorizedError : FeatureProviderException 11 | { 12 | /// 13 | /// Constructor of the exception 14 | /// 15 | /// Message to display 16 | /// Original exception 17 | public UnauthorizedError(string message, Exception innerException = null) : base(ErrorType.General, message, 18 | innerException) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/extensions/GoFeatureFlagExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using OpenFeature.Model; 3 | 4 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.extensions; 5 | 6 | /// 7 | /// Extensions for GO Feature Flag provider. 8 | /// 9 | public static class GoFeatureFlagExtensions 10 | { 11 | /// 12 | /// Convert a Dictionary to an ImmutableMetadata. 13 | /// 14 | /// 15 | /// 16 | public static ImmutableMetadata 17 | ToImmutableMetadata(this Dictionary metadataDictionary) // 'this' keyword is crucial 18 | { 19 | return metadataDictionary != null ? new ImmutableMetadata(metadataDictionary) : null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/hooks/EnrichEvaluationContextHook.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using OpenFeature.Contrib.Providers.GOFeatureFlag.models; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.hooks; 8 | 9 | /// 10 | /// Enrich the evaluation context with additional information 11 | /// 12 | public class EnrichEvaluationContextHook : Hook 13 | { 14 | private readonly Structure _metadata; 15 | 16 | /// 17 | /// Constructor of the Hook 18 | /// 19 | /// metadata to use in order to enrich the evaluation context 20 | public EnrichEvaluationContextHook(ExporterMetadata metadata) 21 | { 22 | _metadata = metadata.AsStructure(); 23 | } 24 | 25 | /// 26 | /// Enrich the evaluation context with additional information before the evaluation of the flag 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | public override ValueTask BeforeAsync(HookContext context, 34 | IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) 35 | { 36 | var builder = EvaluationContext.Builder(); 37 | builder.Merge(context.EvaluationContext); 38 | builder.Set("gofeatureflag", _metadata); 39 | return new ValueTask(builder.Build()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/ExporterMetadata.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Model; 2 | 3 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag.models; 4 | 5 | /// 6 | /// This class represent the exporter metadata that will be sent in your evaluation data collectore 7 | /// 8 | public class ExporterMetadata 9 | { 10 | private readonly StructureBuilder _exporterMetadataBuilder = Structure.Builder(); 11 | 12 | /// 13 | /// Add metadata to the exporter 14 | /// 15 | /// 16 | /// 17 | public void Add(string key, string value) 18 | { 19 | _exporterMetadataBuilder.Set(key, value); 20 | } 21 | 22 | /// 23 | /// Add metadata to the exporter 24 | /// 25 | /// 26 | /// 27 | public void Add(string key, bool value) 28 | { 29 | _exporterMetadataBuilder.Set(key, value); 30 | } 31 | 32 | /// 33 | /// Add metadata to the exporter 34 | /// 35 | /// 36 | /// 37 | public void Add(string key, double value) 38 | { 39 | _exporterMetadataBuilder.Set(key, value); 40 | } 41 | 42 | /// 43 | /// Add metadata to the exporter 44 | /// 45 | /// 46 | /// 47 | public void Add(string key, int value) 48 | { 49 | _exporterMetadataBuilder.Set(key, value); 50 | } 51 | 52 | /// 53 | /// Return the metadata as a structure 54 | /// 55 | /// 56 | public Structure AsStructure() 57 | { 58 | return _exporterMetadataBuilder.Build(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | using OpenFeature.Contrib.Providers.GOFeatureFlag.converters; 4 | using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; 5 | using OpenFeature.Model; 6 | 7 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag; 8 | 9 | /// 10 | /// GO Feature Flag request to be sent to the evaluation API 11 | /// 12 | public class OfrepRequest 13 | { 14 | private const string KeyField = "targetingKey"; 15 | private readonly EvaluationContext _ctx; 16 | 17 | /// 18 | /// Create a new GO Feature Flag request to be sent to the evaluation API 19 | /// 20 | /// 21 | /// 22 | /// 23 | public OfrepRequest(EvaluationContext ctx) 24 | { 25 | try 26 | { 27 | if (ctx is null) 28 | throw new InvalidEvaluationContext("GO Feature Flag need an Evaluation context to work."); 29 | if (!ctx.GetValue(KeyField).IsString) 30 | throw new InvalidTargetingKey("targetingKey field MUST be a string."); 31 | } 32 | catch (KeyNotFoundException e) 33 | { 34 | throw new InvalidTargetingKey("targetingKey field is mandatory.", e); 35 | } 36 | 37 | _ctx = ctx; 38 | } 39 | 40 | /// 41 | /// Returns the JSON request as string to be sent to the API 42 | /// 43 | /// JSON request as string to be sent to the API 44 | public string AsJsonString() 45 | { 46 | var request = new Dictionary { { "context", _ctx.AsDictionary() } }; 47 | return JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace OpenFeature.Contrib.Providers.GOFeatureFlag; 4 | 5 | /// 6 | /// OfrepResponse is the response returned by the OFREP API. 7 | /// 8 | public class OfrepResponse 9 | { 10 | /// 11 | /// value contains the result of the flag. 12 | /// 13 | public object Value { get; set; } 14 | 15 | /// 16 | /// key contains the name of the feature flag. 17 | /// 18 | public string Key { get; set; } 19 | 20 | /// 21 | /// reason used to choose this variation. 22 | /// 23 | public string Reason { get; set; } 24 | 25 | /// 26 | /// variationType contains the name of the variation used for this flag. 27 | /// 28 | public string Variant { get; set; } 29 | 30 | /// 31 | /// cacheable is true if the flag is cacheable. 32 | /// 33 | public bool Cacheable { get; set; } 34 | 35 | /// 36 | /// errorCode is empty if everything went ok. 37 | /// 38 | public string ErrorCode { get; set; } 39 | 40 | /// 41 | /// errorDetails is set only if errorCode is not empty. 42 | /// 43 | public string ErrorDetails { get; set; } 44 | 45 | /// 46 | /// metadata contains the metadata of the flag. 47 | /// 48 | public Dictionary Metadata { get; set; } 49 | } 50 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.GOFeatureFlag/version.txt: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Statsig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.9...OpenFeature.Contrib.Providers.Statsig-v0.1.0) (2024-08-22) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) 9 | 10 | ### ✨ New Features 11 | 12 | * use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) ([f845134](https://github.com/open-feature/dotnet-sdk-contrib/commit/f84513438586457087ac47fd40629912f2ec473a)) 13 | 14 | ## [0.0.9](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.8...OpenFeature.Contrib.Providers.Statsig-v0.0.9) (2024-08-21) 15 | 16 | 17 | ### 🐛 Bug Fixes 18 | 19 | * version expression ([cad2cd1](https://github.com/open-feature/dotnet-sdk-contrib/commit/cad2cd166d0c25753b37189f044c3a585cda0fad)) 20 | 21 | ## [0.0.8](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.7...OpenFeature.Contrib.Providers.Statsig-v0.0.8) (2024-08-20) 22 | 23 | 24 | ### 🧹 Chore 25 | 26 | * update OpenFeature version compatiblity ([#249](https://github.com/open-feature/dotnet-sdk-contrib/issues/249)) ([232e948](https://github.com/open-feature/dotnet-sdk-contrib/commit/232e948a0916ca10612f85343e2eecebca107090)) 27 | 28 | ## [0.0.7](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.6...OpenFeature.Contrib.Providers.Statsig-v0.0.7) (2024-06-19) 29 | 30 | 31 | ### 🧹 Chore 32 | 33 | * **deps:** update dependency statsig to v1.26.0 ([#214](https://github.com/open-feature/dotnet-sdk-contrib/issues/214)) ([34015fa](https://github.com/open-feature/dotnet-sdk-contrib/commit/34015fa816fce09c44cc7acc802097053ab98d9a)) 34 | 35 | ## [0.0.6](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.5...OpenFeature.Contrib.Providers.Statsig-v0.0.6) (2024-06-07) 36 | 37 | 38 | ### ✨ New Features 39 | 40 | * map to Statsig CustomIDs ([#210](https://github.com/open-feature/dotnet-sdk-contrib/issues/210)) ([c745edc](https://github.com/open-feature/dotnet-sdk-contrib/commit/c745edc1a2d1141b2ef41b7b661fdd68b764c57d)) 41 | 42 | ## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.4...OpenFeature.Contrib.Providers.Statsig-v0.0.5) (2024-05-08) 43 | 44 | 45 | ### 🧹 Chore 46 | 47 | * **deps:** update dependency statsig to v1.25.0 ([#198](https://github.com/open-feature/dotnet-sdk-contrib/issues/198)) ([f2583d4](https://github.com/open-feature/dotnet-sdk-contrib/commit/f2583d4b3d47de703a5b59f20053c603f9bb3874)) 48 | 49 | ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.3...OpenFeature.Contrib.Providers.Statsig-v0.0.4) (2024-04-16) 50 | 51 | 52 | ### 🐛 Bug Fixes 53 | 54 | * Fix Statsig nuget package name ([#172](https://github.com/open-feature/dotnet-sdk-contrib/issues/172)) ([3d089f5](https://github.com/open-feature/dotnet-sdk-contrib/commit/3d089f5c48478d7151fcf5964aa545471a0afe5c)) 55 | * Use new Statsig Api to return default value when flag is not defined ([#177](https://github.com/open-feature/dotnet-sdk-contrib/issues/177)) ([5efc8a6](https://github.com/open-feature/dotnet-sdk-contrib/commit/5efc8a603d1ad9d8887d75e38f95d5168a2319fa)) 56 | 57 | ## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.2...OpenFeature.Contrib.Providers.Statsig-v0.0.3) (2024-04-03) 58 | 59 | 60 | ### 🐛 Bug Fixes 61 | 62 | * Update usage of StatsigServerOptions ([#169](https://github.com/open-feature/dotnet-sdk-contrib/issues/169)) ([d12bbc7](https://github.com/open-feature/dotnet-sdk-contrib/commit/d12bbc735eda7c2931d7f8d6ad32ef4f2f1741ed)) 63 | 64 | 65 | ### 🧹 Chore 66 | 67 | * **deps:** update dependency statsig to v1.24.0 ([#167](https://github.com/open-feature/dotnet-sdk-contrib/issues/167)) ([f5c80e9](https://github.com/open-feature/dotnet-sdk-contrib/commit/f5c80e923ef96760c951ae209a818004ed8bfb1b)) 68 | 69 | ## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Statsig-v0.0.1...OpenFeature.Contrib.Providers.Statsig-v0.0.2) (2024-03-14) 70 | 71 | 72 | ### ✨ New Features 73 | 74 | * Statsing provider ([#163](https://github.com/open-feature/dotnet-sdk-contrib/issues/163)) ([98028e9](https://github.com/open-feature/dotnet-sdk-contrib/commit/98028e9c37bce6225a1feeef09917a4539065a23)) 75 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Error; 2 | using OpenFeature.Model; 3 | using Statsig; 4 | 5 | namespace OpenFeature.Contrib.Providers.Statsig; 6 | 7 | internal static class EvaluationContextExtensions 8 | { 9 | //These keys match the keys of the statsiguser object as described here 10 | //https://docs.statsig.com/client/concepts/user 11 | internal const string CONTEXT_APP_VERSION = "appVersion"; 12 | internal const string CONTEXT_COUNTRY = "country"; 13 | internal const string CONTEXT_EMAIL = "email"; 14 | internal const string CONTEXT_IP = "ip"; 15 | internal const string CONTEXT_LOCALE = "locale"; 16 | internal const string CONTEXT_USER_AGENT = "userAgent"; 17 | internal const string CONTEXT_PRIVATE_ATTRIBUTES = "privateAttributes"; 18 | internal const string CONTEXT_CUSTOM_IDS = "customIDs"; 19 | 20 | public static StatsigUser AsStatsigUser(this EvaluationContext evaluationContext) 21 | { 22 | if (evaluationContext == null) 23 | return null; 24 | 25 | var user = new StatsigUser() { UserID = evaluationContext.TargetingKey }; 26 | foreach (var item in evaluationContext) 27 | { 28 | switch (item.Key) 29 | { 30 | case CONTEXT_APP_VERSION: 31 | user.AppVersion = item.Value.AsString; 32 | break; 33 | case CONTEXT_COUNTRY: 34 | user.Country = item.Value.AsString; 35 | break; 36 | case CONTEXT_EMAIL: 37 | user.Email = item.Value.AsString; 38 | break; 39 | case CONTEXT_IP: 40 | user.IPAddress = item.Value.AsString; 41 | break; 42 | case CONTEXT_USER_AGENT: 43 | user.UserAgent = item.Value.AsString; 44 | break; 45 | case CONTEXT_LOCALE: 46 | user.Locale = item.Value.AsString; 47 | break; 48 | case CONTEXT_PRIVATE_ATTRIBUTES: 49 | if (item.Value.IsStructure) 50 | { 51 | var privateAttributes = item.Value.AsStructure; 52 | foreach (var items in privateAttributes) 53 | { 54 | user.AddPrivateAttribute(items.Key, items.Value.AsObject); 55 | } 56 | } 57 | break; 58 | case CONTEXT_CUSTOM_IDS: 59 | if (item.Value.IsStructure) 60 | { 61 | var customIds = item.Value.AsStructure; 62 | foreach (var customId in customIds) 63 | { 64 | if (customId.Value.IsString) 65 | user.AddCustomID(customId.Key, customId.Value.AsString); 66 | else throw new FeatureProviderException(Constant.ErrorType.TypeMismatch, "Only string values are supported for CustomIDs"); 67 | } 68 | } 69 | break; 70 | 71 | default: 72 | user.AddCustomProperty(item.Key, item.Value.AsObject); 73 | break; 74 | } 75 | } 76 | return user; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Statsig/OpenFeature.Contrib.Providers.Statsig.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | OpenFeature.Contrib.Providers.Statsig 5 | 0.1.0 6 | $(VersionNumber) 7 | $(VersionNumber) 8 | $(VersionNumber) 9 | Statsig provider for .NET 10 | README.md 11 | Jens Kjær Henneberg 12 | true 13 | 14 | 15 | 16 | 17 | <_Parameter1>$(MSBuildProjectName).Test 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Statsig/README.md: -------------------------------------------------------------------------------- 1 | # Statsig Feature Flag .NET Provider 2 | 3 | The Statsig Flag provider allows you to connect to Statsig. Please note this is a minimal implementation - only `ResolveBooleanValueAsync` is implemented. 4 | 5 | # .Net SDK usage 6 | 7 | ## Requirements 8 | 9 | - open-feature/dotnet-sdk v1.5.0 > v2.0.0 10 | 11 | ## Install dependencies 12 | 13 | The first things we will do is install the **Open Feature SDK** and the **Statsig Feature Flag provider**. 14 | 15 | ### .NET Cli 16 | ```shell 17 | dotnet add package OpenFeature.Contrib.Providers.Statsig 18 | ``` 19 | ### Package Manager 20 | 21 | ```shell 22 | NuGet\Install-Package OpenFeature.Contrib.Providers.Statsig 23 | ``` 24 | ### Package Reference 25 | 26 | ```xml 27 | 28 | ``` 29 | ### Packet cli 30 | 31 | ```shell 32 | paket add OpenFeature.Contrib.Providers.Statsig 33 | ``` 34 | 35 | ### Cake 36 | 37 | ```shell 38 | // Install OpenFeature.Contrib.Providers.Statsig as a Cake Addin 39 | #addin nuget:?package= OpenFeature.Contrib.Providers.Statsig 40 | 41 | // Install OpenFeature.Contrib.Providers.Statsig as a Cake Tool 42 | #tool nuget:?package= OpenFeature.Contrib.Providers.Statsig 43 | ``` 44 | 45 | ## Using the Statsig Provider with the OpenFeature SDK 46 | 47 | The following example shows how to use the Statsig provider with the OpenFeature SDK. 48 | 49 | ```csharp 50 | using OpenFeature; 51 | using OpenFeature.Contrib.Providers.Statsig; 52 | using System; 53 | 54 | StatsigProvider statsigProvider = new StatsigProvider("#YOUR-SDK-KEY#"); 55 | 56 | // Set the statsigProvider as the provider for the OpenFeature SDK 57 | await Api.Instance.SetProviderAsync(statsigProvider); 58 | 59 | var eb = EvaluationContext.Builder(); 60 | eb.SetTargetingKey("john@doe.acme"); 61 | 62 | IFeatureClient client = Api.Instance.GetClient(context: eb.Build()); 63 | 64 | bool isMyAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isMyAwesomeFeatureEnabled", false); 65 | 66 | if (isMyAwesomeFeatureEnabled) 67 | { 68 | Console.WriteLine("New Feature enabled!"); 69 | } 70 | 71 | ``` 72 | 73 | ### Customizing the Statsig Provider 74 | 75 | The Statsig provider can be customized by passing a `StatsigServerOptions` object to the constructor. 76 | 77 | ```csharp 78 | var statsigProvider = new StatsigProvider("#YOUR-SDK-KEY#", new StatsigServerOptions() { LocalMode = true }); 79 | ``` 80 | 81 | For a full list of options see the [Statsig documentation](https://docs.statsig.com/server/dotnetSDK#statsig-options). 82 | 83 | ## EvaluationContext and Statsig User relationship 84 | 85 | Statsig has the concept of a [StatsigUser](https://docs.statsig.com/client/concepts/user) where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an EvaluationContext which is a dictionary of string keys and values. The Statsig provider will map the EvaluationContext to a StatsigUser. 86 | 87 | The following parameters are mapped to the corresponding Statsig pre-defined parameters 88 | 89 | | EvaluationContext Key | Statsig User Parameter | 90 | |-----------------------|---------------------------| 91 | | `appVersion` | `AppVersion` | 92 | | `country` | `Country` | 93 | | `email` | `Email` | 94 | | `ip` | `Ip` | 95 | | `locale` | `Locale` | 96 | | `userAgent` | `UserAgent` | 97 | | `privateAttributes` | `PrivateAttributes` | 98 | 99 | ## Known issues and limitations 100 | - Only `ResolveBooleanValueAsync` implemented for now 101 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Statsig/StatsigProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using OpenFeature.Constant; 5 | using OpenFeature.Model; 6 | using Statsig; 7 | using Statsig.Server; 8 | using Statsig.Server.Evaluation; 9 | 10 | namespace OpenFeature.Contrib.Providers.Statsig; 11 | 12 | /// 13 | /// An OpenFeature which enables the use of the Statsig Server-Side SDK for .NET 14 | /// with OpenFeature. 15 | /// 16 | /// 17 | /// var provider = new StatsigProvider("my-sdk-key"), new StatsigProviderOptions(){LocalMode = false}); 18 | /// 19 | /// OpenFeature.Api.Instance.SetProvider(provider); 20 | /// 21 | /// var client = OpenFeature.Api.Instance.GetClient(); 22 | /// 23 | public sealed class StatsigProvider : FeatureProvider 24 | { 25 | private readonly Metadata _providerMetadata = new Metadata("Statsig provider"); 26 | private readonly string _sdkKey = "secret-"; //Dummy sdk key that works with local mode 27 | internal readonly ServerDriver ServerDriver; 28 | 29 | /// 30 | /// Creates new instance of 31 | /// 32 | /// SDK Key to access Statsig. 33 | /// The StatsigServerOptions to configure the provider. 34 | public StatsigProvider(string sdkKey = null, StatsigServerOptions statsigServerOptions = null) 35 | { 36 | if (sdkKey != null) 37 | { 38 | _sdkKey = sdkKey; 39 | } 40 | ServerDriver = new ServerDriver(_sdkKey, statsigServerOptions); 41 | } 42 | 43 | /// 44 | public override Metadata GetMetadata() => _providerMetadata; 45 | 46 | /// 47 | public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) 48 | { 49 | var result = ServerDriver.GetFeatureGate(context.AsStatsigUser(), flagKey); 50 | var gateFound = false; 51 | var responseType = ErrorType.None; 52 | 53 | switch (result.Reason) 54 | { 55 | case EvaluationReason.Network: 56 | case EvaluationReason.LocalOverride: 57 | case EvaluationReason.Bootstrap: 58 | case EvaluationReason.DataAdapter: 59 | gateFound = true; 60 | break; 61 | case EvaluationReason.Unrecognized: 62 | responseType = ErrorType.FlagNotFound; 63 | break; 64 | case EvaluationReason.Uninitialized: 65 | responseType = ErrorType.ProviderNotReady; 66 | break; 67 | case EvaluationReason.Unsupported: 68 | responseType = ErrorType.InvalidContext; 69 | break; 70 | case EvaluationReason.Error: 71 | responseType = ErrorType.General; 72 | break; 73 | case null: 74 | break; 75 | } 76 | return Task.FromResult(new ResolutionDetails(flagKey, gateFound ? result.Value : defaultValue, responseType)); 77 | } 78 | 79 | /// 80 | public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) 81 | { 82 | throw new NotImplementedException(); 83 | } 84 | 85 | /// 86 | public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) 87 | { 88 | throw new NotImplementedException(); 89 | } 90 | 91 | /// 92 | public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) 93 | { 94 | throw new NotImplementedException(); 95 | } 96 | 97 | /// 98 | public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) 99 | { 100 | throw new NotImplementedException(); 101 | } 102 | 103 | /// 104 | public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) 105 | { 106 | await ServerDriver.Initialize().ConfigureAwait(false); 107 | } 108 | 109 | /// 110 | public override Task ShutdownAsync(CancellationToken cancellationToken = default) 111 | { 112 | return ServerDriver.Shutdown(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/OpenFeature.Contrib.Providers.Statsig/version.txt: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using OpenFeature.Model; 6 | using OpenTelemetry; 7 | using OpenTelemetry.Metrics; 8 | using OpenTelemetry.Resources; 9 | using Xunit; 10 | 11 | namespace OpenFeature.Contrib.Hooks.Otel.Test; 12 | 13 | public class MetricsHookTest 14 | { 15 | readonly List exportedItems; 16 | readonly MeterProvider meterProvider; 17 | HookContext hookContext = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), EvaluationContext.Empty); 18 | 19 | public MetricsHookTest() 20 | { 21 | exportedItems = new List(); 22 | meterProvider = Sdk.CreateMeterProviderBuilder() 23 | .AddMeter("OpenFeature.Contrib.Hooks.Otel") 24 | .ConfigureResource(r => r.AddService("openfeature")) 25 | .AddInMemoryExporter(exportedItems) 26 | .Build(); 27 | } 28 | 29 | [Fact] 30 | public async Task After_Test() 31 | { 32 | // Arrange 33 | const string metricName = "feature_flag.evaluation_success_total"; 34 | var otelHook = new MetricsHook(); 35 | 36 | // Act 37 | await otelHook.AfterAsync(hookContext, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), new Dictionary()); 38 | 39 | // Flush metrics 40 | meterProvider.ForceFlush(); 41 | 42 | // Assert metrics 43 | Assert.NotEmpty(exportedItems); 44 | 45 | // check if the metric is present in the exported items 46 | var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); 47 | Assert.NotNull(metric); 48 | 49 | var noOtherMetric = exportedItems.All(m => m.Name == metricName); 50 | Assert.True(noOtherMetric); 51 | } 52 | 53 | [Fact] 54 | public async Task Error_Test() 55 | { 56 | // Arrange 57 | const string metricName = "feature_flag.evaluation_error_total"; 58 | var otelHook = new MetricsHook(); 59 | 60 | // Act 61 | await otelHook.ErrorAsync(hookContext, new Exception(), new Dictionary()); 62 | 63 | // Flush metrics 64 | meterProvider.ForceFlush(); 65 | 66 | // Assert metrics 67 | Assert.NotEmpty(exportedItems); 68 | 69 | // check if the metric is present in the exported items 70 | var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); 71 | Assert.NotNull(metric); 72 | 73 | var noOtherMetric = exportedItems.All(m => m.Name == metricName); 74 | Assert.True(noOtherMetric); 75 | } 76 | 77 | [Fact] 78 | public async Task Finally_Test() 79 | { 80 | // Arrange 81 | const string metricName = "feature_flag.evaluation_active_count"; 82 | var otelHook = new MetricsHook(); 83 | 84 | // Act 85 | await otelHook.FinallyAsync(hookContext, new Dictionary()); 86 | 87 | // Flush metrics 88 | meterProvider.ForceFlush(); 89 | 90 | // Assert metrics 91 | Assert.NotEmpty(exportedItems); 92 | 93 | // check if the metric feature_flag.evaluation_success_total is present in the exported items 94 | var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); 95 | Assert.NotNull(metric); 96 | 97 | var noOtherMetric = exportedItems.All(m => m.Name == metricName); 98 | Assert.True(noOtherMetric); 99 | } 100 | 101 | [Fact] 102 | public async Task Before_Test() 103 | { 104 | 105 | // Arrange 106 | const string metricName1 = "feature_flag.evaluation_active_count"; 107 | const string metricName2 = "feature_flag.evaluation_requests_total"; 108 | var otelHook = new MetricsHook(); 109 | 110 | // Act 111 | await otelHook.BeforeAsync(hookContext, new Dictionary()); 112 | 113 | // Flush metrics 114 | meterProvider.ForceFlush(); 115 | 116 | // Assert metrics 117 | Assert.NotEmpty(exportedItems); 118 | 119 | // check if the metric is present in the exported items 120 | var metric1 = exportedItems.FirstOrDefault(m => m.Name == metricName1); 121 | Assert.NotNull(metric1); 122 | 123 | var metric2 = exportedItems.FirstOrDefault(m => m.Name == metricName2); 124 | Assert.NotNull(metric2); 125 | 126 | var noOtherMetric = exportedItems.All(m => m.Name == metricName1 || m.Name == metricName2); 127 | Assert.True(noOtherMetric); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.ConfigCat.Test/OpenFeature.Contrib.Providers.ConfigCat.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.ConfigCat.Test/UserBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Model; 2 | using Xunit; 3 | 4 | namespace OpenFeature.Contrib.ConfigCat.Test; 5 | 6 | public class UserBuilderTests 7 | { 8 | [Theory] 9 | [InlineData("id", "test")] 10 | [InlineData("identifier", "test")] 11 | public void UserBuilder_Should_Map_Identifiers(string key, string value) 12 | { 13 | // Arrange 14 | var context = EvaluationContext.Builder().Set(key, value).Build(); 15 | 16 | // Act 17 | var user = context.BuildUser(); 18 | 19 | // Assert 20 | Assert.Equal(value, user.Identifier); 21 | } 22 | 23 | [Fact] 24 | public void UserBuilder_Should_Map_Email() 25 | { 26 | // Arrange 27 | var context = EvaluationContext.Builder().Set("email", "email@email.com").Build(); 28 | 29 | // Act 30 | var user = context.BuildUser(); 31 | 32 | // Assert 33 | Assert.Equal("email@email.com", user.Email); 34 | } 35 | 36 | [Fact] 37 | public void UserBuilder_Should_Map_Country() 38 | { 39 | // Arrange 40 | var context = EvaluationContext.Builder().Set("country", "US").Build(); 41 | 42 | // Act 43 | var user = context.BuildUser(); 44 | 45 | // Assert 46 | Assert.Equal("US", user.Country); 47 | } 48 | 49 | [Fact] 50 | public void UserBuilder_Should_Map_Custom() 51 | { 52 | // Arrange 53 | var context = EvaluationContext.Builder().Set("custom", "custom").Build(); 54 | 55 | // Act 56 | var user = context.BuildUser(); 57 | 58 | // Assert 59 | Assert.Equal("custom", user.Custom["custom"]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.EnvVar.Test/OpenFeature.Contrib.Providers.EnvVar.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.FeatureManagement.Test/FeatureManagementProviderSimpleFlagTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Configuration; 3 | using Xunit; 4 | 5 | namespace OpenFeature.Contrib.Providers.FeatureManagement.Test; 6 | 7 | public class FeatureManagementProviderSimpleFlagTest 8 | { 9 | [Theory] 10 | [MemberData(nameof(TestData.BooleanSimple), MemberType = typeof(TestData))] 11 | public async Task BooleanValue_ShouldReturnExpected(string key, bool defaultValue, bool expectedValue) 12 | { 13 | // Arrange 14 | var configuration = new ConfigurationBuilder() 15 | .AddJsonFile("appsettings.enabled.json") 16 | .Build(); 17 | 18 | var provider = new FeatureManagementProvider(configuration); 19 | 20 | // Act 21 | // Invert the expected value to ensure that the value is being read from the configuration 22 | var result = await provider.ResolveBooleanValueAsync(key, defaultValue); 23 | 24 | // Assert 25 | Assert.Equal(expectedValue, result.Value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.FeatureManagement.Test/FeatureManagementProviderTestNoContext.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Configuration; 3 | using Xunit; 4 | 5 | namespace OpenFeature.Contrib.Providers.FeatureManagement.Test; 6 | 7 | public class FeatureManagementProviderTestNoContext 8 | { 9 | [Theory] 10 | [MemberData(nameof(TestData.BooleanNoContext), MemberType = typeof(TestData))] 11 | public async Task BooleanValue_ShouldReturnExpected(string key, bool defaultValue, bool expectedValue) 12 | { 13 | // Arrange 14 | var configuration = new ConfigurationBuilder() 15 | .AddJsonFile("appsettings.enabled.json") 16 | .Build(); 17 | 18 | var provider = new FeatureManagementProvider(configuration); 19 | 20 | // Act 21 | // Invert the expected value to ensure that the value is being read from the configuration 22 | var result = await provider.ResolveBooleanValueAsync(key, defaultValue); 23 | 24 | // Assert 25 | Assert.Equal(expectedValue, result.Value); 26 | } 27 | 28 | [Theory] 29 | [MemberData(nameof(TestData.DoubleNoContext), MemberType = typeof(TestData))] 30 | public async Task DoubleValue_ShouldReturnExpected(string key, double defaultValue, double expectedValue) 31 | { 32 | // Arrange 33 | var configuration = new ConfigurationBuilder() 34 | .AddJsonFile("appsettings.enabled.json") 35 | .Build(); 36 | var provider = new FeatureManagementProvider(configuration); 37 | 38 | // Act 39 | // Using 0.0 for the default to verify the value is being read from the configuration 40 | var result = await provider.ResolveDoubleValueAsync(key, defaultValue); 41 | 42 | // Assert 43 | Assert.Equal(expectedValue, result.Value); 44 | } 45 | 46 | [Theory] 47 | [MemberData(nameof(TestData.IntegerNoContext), MemberType = typeof(TestData))] 48 | public async Task IntegerValue_ShouldReturnExpected(string key, int defaultValue, int expectedValue) 49 | { 50 | // Arrange 51 | var configuration = new ConfigurationBuilder() 52 | .AddJsonFile("appsettings.enabled.json") 53 | .Build(); 54 | var provider = new FeatureManagementProvider(configuration); 55 | 56 | // Act 57 | // Using 0 for the default to verify the value is being read from the configuration 58 | var result = await provider.ResolveIntegerValueAsync(key, defaultValue); 59 | 60 | // Assert 61 | Assert.Equal(expectedValue, result.Value); 62 | } 63 | 64 | [Theory] 65 | [MemberData(nameof(TestData.StringNoContext), MemberType = typeof(TestData))] 66 | public async Task StringValue_ShouldReturnExpected(string key, string defaultValue, string expectedValue) 67 | { 68 | // Arrange 69 | var configuration = new ConfigurationBuilder() 70 | .AddJsonFile("appsettings.enabled.json") 71 | .Build(); 72 | var provider = new FeatureManagementProvider(configuration); 73 | 74 | // Act 75 | // Using 0 for the default to verify the value is being read from the configuration 76 | var result = await provider.ResolveStringValueAsync(key, defaultValue); 77 | 78 | // Assert 79 | Assert.Equal(expectedValue, result.Value); 80 | } 81 | 82 | [Theory] 83 | [MemberData(nameof(TestData.StructureNoContext), MemberType = typeof(TestData))] 84 | public async Task StructureValue_ShouldReturnExpected(string key) 85 | { 86 | // Arrange 87 | var configuration = new ConfigurationBuilder() 88 | .AddJsonFile("appsettings.enabled.json") 89 | .Build(); 90 | var provider = new FeatureManagementProvider(configuration); 91 | 92 | // Act 93 | // Using 0 for the default to verify the value is being read from the configuration 94 | var result = await provider.ResolveStructureValueAsync(key, null); 95 | 96 | // Assert 97 | Assert.NotNull(result); 98 | Assert.NotNull(result.Value); 99 | Assert.True(result.Value.IsStructure); 100 | Assert.Equal(2, result.Value.AsStructure.Count); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.FeatureManagement.Test/OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | Always 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.Common/OpenFeature.Contrib.Providers.Flagd.E2e.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Features/.gitignore: -------------------------------------------------------------------------------- 1 | *.feature 2 | *.cs 3 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Features/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/dotnet-sdk-contrib/741bfc0d8c9a2ab33bfe658b2279cbaae018779f/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Features/.gitkeep -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Builders; 2 | using DotNet.Testcontainers.Containers; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest; 5 | 6 | public class FlagdSyncTestBedContainer 7 | { 8 | public IContainer Container { get; } 9 | 10 | public FlagdSyncTestBedContainer() 11 | { 12 | Container = new ContainerBuilder() 13 | .WithImage("ghcr.io/open-feature/flagd-testbed:v0.5.21") 14 | .WithPortBinding(8015, true) 15 | .Build(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Contrib.Providers.Flagd.E2e.Common; 2 | using Reqnroll; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps; 5 | 6 | [Binding, Scope(Feature = "Flag evaluation")] 7 | public class EvaluationStepDefinitionsProcess : EvaluationStepDefinitionsBase 8 | { 9 | static EvaluationStepDefinitionsProcess() 10 | { 11 | var host = TestHooks.FlagdSyncTestBed.Container.Hostname; 12 | var port = TestHooks.FlagdSyncTestBed.Container.GetMappedPublicPort(8015); 13 | 14 | var flagdProvider = new FlagdProvider( 15 | FlagdConfig.Builder() 16 | .WithHost(host) 17 | .WithPort(port) 18 | .WithResolverType(ResolverType.IN_PROCESS) 19 | .Build() 20 | ); 21 | 22 | Api.Instance.SetProviderAsync("process-test-evaluation", flagdProvider).Wait(5000); 23 | } 24 | 25 | public EvaluationStepDefinitionsProcess(ScenarioContext scenarioContext) : base(scenarioContext) 26 | { 27 | client = Api.Instance.GetClient("process-test-evaluation"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Contrib.Providers.Flagd.E2e.Common; 2 | using Reqnroll; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps; 5 | 6 | [Binding] 7 | [Scope(Feature = "flagd providers")] 8 | [Scope(Feature = "flagd json evaluation")] 9 | public class FlagdStepDefinitionsProcess : FlagdStepDefinitionsBase 10 | { 11 | static FlagdStepDefinitionsProcess() 12 | { 13 | var host = TestHooks.FlagdSyncTestBed.Container.Hostname; 14 | var port = TestHooks.FlagdSyncTestBed.Container.GetMappedPublicPort(8015); 15 | 16 | var flagdProvider = new FlagdProvider( 17 | FlagdConfig.Builder() 18 | .WithHost(host) 19 | .WithPort(port) 20 | .WithResolverType(ResolverType.IN_PROCESS) 21 | .Build() 22 | ); 23 | 24 | Api.Instance.SetProviderAsync("process-test-flagd", flagdProvider).Wait(5000); 25 | } 26 | 27 | public FlagdStepDefinitionsProcess(ScenarioContext scenarioContext) : base(scenarioContext) 28 | { 29 | client = Api.Instance.GetClient("process-test-flagd"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Reqnroll; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps; 5 | 6 | [Binding] 7 | public class TestHooks 8 | { 9 | public static FlagdSyncTestBedContainer FlagdSyncTestBed { get; private set; } 10 | 11 | [BeforeTestRun] 12 | public static async Task StartContainerAsync() 13 | { 14 | FlagdSyncTestBed = new FlagdSyncTestBedContainer(); 15 | 16 | await FlagdSyncTestBed.Container.StartAsync().ConfigureAwait(false); 17 | } 18 | 19 | [AfterTestRun] 20 | public static async Task StopContainerAsync() 21 | { 22 | if (FlagdSyncTestBed != null) 23 | { 24 | await FlagdSyncTestBed.Container.StopAsync().ConfigureAwait(false); 25 | await FlagdSyncTestBed.Container.DisposeAsync().ConfigureAwait(false); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Features/.gitignore: -------------------------------------------------------------------------------- 1 | *.feature 2 | *.cs 3 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Features/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/dotnet-sdk-contrib/741bfc0d8c9a2ab33bfe658b2279cbaae018779f/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Features/.gitkeep -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs: -------------------------------------------------------------------------------- 1 | using DotNet.Testcontainers.Builders; 2 | using DotNet.Testcontainers.Containers; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest; 5 | 6 | public class FlagdRpcTestBedContainer 7 | { 8 | public IContainer Container { get; } 9 | 10 | public FlagdRpcTestBedContainer() 11 | { 12 | Container = new ContainerBuilder() 13 | .WithImage("ghcr.io/open-feature/flagd-testbed:v0.5.21") 14 | .WithPortBinding(8013, true) 15 | .Build(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Contrib.Providers.Flagd.E2e.Common; 2 | using Reqnroll; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps; 5 | 6 | [Binding, Scope(Feature = "Flag evaluation")] 7 | public class EvaluationStepDefinitionsRpc : EvaluationStepDefinitionsBase 8 | { 9 | static EvaluationStepDefinitionsRpc() 10 | { 11 | var host = TestHooks.FlagdTestBed.Container.Hostname; 12 | var port = TestHooks.FlagdTestBed.Container.GetMappedPublicPort(8013); 13 | 14 | var flagdProvider = new FlagdProvider( 15 | FlagdConfig.Builder() 16 | .WithHost(host) 17 | .WithPort(port) 18 | .Build() 19 | ); 20 | 21 | Api.Instance.SetProviderAsync("rpc-test-evaluation", flagdProvider).Wait(5000); 22 | } 23 | 24 | public EvaluationStepDefinitionsRpc(ScenarioContext scenarioContext) : base(scenarioContext) 25 | { 26 | client = Api.Instance.GetClient("rpc-test-evaluation"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature.Contrib.Providers.Flagd.E2e.Common; 2 | using Reqnroll; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps; 5 | 6 | [Binding] 7 | [Scope(Feature = "flagd providers")] 8 | [Scope(Feature = "flagd json evaluation")] 9 | public class FlagdStepDefinitionsRpc : FlagdStepDefinitionsBase 10 | { 11 | static FlagdStepDefinitionsRpc() 12 | { 13 | var host = TestHooks.FlagdTestBed.Container.Hostname; 14 | var port = TestHooks.FlagdTestBed.Container.GetMappedPublicPort(8013); 15 | 16 | var flagdProvider = new FlagdProvider( 17 | FlagdConfig.Builder() 18 | .WithHost(host) 19 | .WithPort(port) 20 | .Build() 21 | ); 22 | 23 | Api.Instance.SetProviderAsync("rpc-test-flagd", flagdProvider).Wait(5000); 24 | } 25 | 26 | public FlagdStepDefinitionsRpc(ScenarioContext scenarioContext) : base(scenarioContext) 27 | { 28 | client = Api.Instance.GetClient("rpc-test-flagd"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Reqnroll; 3 | 4 | namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps; 5 | 6 | [Binding] 7 | public class TestHooks 8 | { 9 | public static FlagdRpcTestBedContainer FlagdTestBed { get; private set; } 10 | 11 | [BeforeTestRun] 12 | public static async Task StartContainerAsync() 13 | { 14 | FlagdTestBed = new FlagdRpcTestBedContainer(); 15 | 16 | await FlagdTestBed.Container.StartAsync().ConfigureAwait(false); 17 | } 18 | 19 | [AfterTestRun] 20 | public static async Task StopContainerAsync() 21 | { 22 | if (FlagdTestBed != null) 23 | { 24 | await FlagdTestBed.Container.StopAsync().ConfigureAwait(false); 25 | await FlagdTestBed.Container.DisposeAsync().ConfigureAwait(false); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.Test/OpenFeature.Contrib.Providers.Flagd.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagd.Test/mycert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBnzCCAQCgAwIBAgIJAKO4QdE4u3u6MAoGCCqGSM49BAMCMBExDzANBgNVBAMTBmZvb2JhcjAe 3 | Fw0yNTA1MjExODIxMjZaFw0zMDA1MjExODIxMjZaMBExDzANBgNVBAMTBmZvb2JhcjCBmzAQBgcq 4 | hkjOPQIBBgUrgQQAIwOBhgAEAPqb3forqfLqohV7itl0/7+OTO+zPRTIJuNFON+kgFM+KLVR6sqm 5 | fWgZOV/oXWEX9j+QhN6OsPTfCdGjSmqrl3FWAEJioyBpy5BOx/o3rLbKTVbrLBOfnh/S0v3fRpyO 6 | FHaU8b1Fnrn3MtZhYFAgALcTeRSvlJY+jpBcDJSXVeC7TB2BMAoGCCqGSM49BAMCA4GMADCBiAJC 7 | AZsUIObTVPdOF71+CLqL5nN67dfiyb1HbiJTcxptPDNn9Nfvju1Cq8vQFhszrUqH/aXKab2rFKXu 8 | QA9X8zCuTmBDAkIA2wkJ7jgUk8AwAHPU0wT7CeVyoTbNMlONJ3c9BVUdgiP3NTFP2lyPi0WGgjnO 9 | BehOzbitZzSzh5Wohtoz1Z/j/SI= 10 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flagsmith.Test/OpenFeature.Contrib.Providers.Flagsmith.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Statsig.Test/OpenFeature.Contrib.Providers.Statsig.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AutoFixture.Xunit2; 3 | using OpenFeature.Model; 4 | using Statsig; 5 | using Xunit; 6 | 7 | namespace OpenFeature.Contrib.Providers.Statsig.Test; 8 | 9 | public class StatsigProviderTest 10 | { 11 | private StatsigProvider statsigProvider; 12 | 13 | public StatsigProviderTest() 14 | { 15 | statsigProvider = new StatsigProvider("secret-", new StatsigServerOptions() { LocalMode = true }); 16 | } 17 | 18 | [Theory] 19 | [InlineAutoData(true, true)] 20 | [InlineAutoData(false, false)] 21 | public async Task GetBooleanValueAsync_ForFeatureWithContext(bool flagValue, bool expectedValue, string userId, string flagName) 22 | { 23 | // Arrange 24 | await statsigProvider.InitializeAsync(null); 25 | var ec = EvaluationContext.Builder().SetTargetingKey(userId).Build(); 26 | statsigProvider.ServerDriver.OverrideGate(flagName, flagValue, userId); 27 | 28 | // Act 29 | var result = await statsigProvider.ResolveBooleanValueAsync(flagName, false, ec); 30 | 31 | // Assert 32 | Assert.Equal(expectedValue, result.Value); 33 | } 34 | 35 | [Theory] 36 | [InlineAutoData(true, false)] 37 | [InlineAutoData(false, false)] 38 | public async Task GetBooleanValueAsync_ForFeatureWithNoContext_ReturnsDefaultValue(bool flagValue, bool defaultValue, string flagName) 39 | { 40 | // Arrange 41 | await statsigProvider.InitializeAsync(null); 42 | statsigProvider.ServerDriver.OverrideGate(flagName, flagValue); 43 | 44 | // Act 45 | var result = await statsigProvider.ResolveBooleanValueAsync(flagName, defaultValue); 46 | 47 | // Assert 48 | Assert.Equal(defaultValue, result.Value); 49 | } 50 | 51 | [Theory] 52 | [AutoData] 53 | [InlineAutoData(false)] 54 | [InlineAutoData(true)] 55 | public async Task GetBooleanValueAsync_ForFeatureWithDefault(bool defaultValue, string flagName, string userId) 56 | { 57 | // Arrange 58 | await statsigProvider.InitializeAsync(null); 59 | 60 | var ec = EvaluationContext.Builder().SetTargetingKey(userId).Build(); 61 | 62 | // Act 63 | var result = await statsigProvider.ResolveBooleanValueAsync(flagName, defaultValue, ec); 64 | 65 | //Assert 66 | Assert.Equal(defaultValue, result.Value); 67 | } 68 | 69 | [Fact] 70 | public async Task TestConcurrentInitilization_DoesntThrowException() 71 | { 72 | // Arrange 73 | var concurrencyTestClass = new StatsigProvider(); 74 | const int numberOfThreads = 50; 75 | 76 | // Act & Assert 77 | var tasks = new Task[numberOfThreads]; 78 | for (int i = 0; i < numberOfThreads; i++) 79 | { 80 | tasks[i] = concurrencyTestClass.InitializeAsync(null); 81 | } 82 | 83 | await Task.WhenAll(tasks); 84 | } 85 | } 86 | --------------------------------------------------------------------------------