From 218ed825edd80c983bddd7329ce2622ad60e8160 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:42:55 +0000 Subject: [PATCH 01/58] chore(deps): update dependency reqnroll.xunit to 3.3.1 (#683) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b856f939..bbdb9186 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,7 +43,7 @@ - + From 6b99bc3eaf1be97f2a4eeedea282f33165fd3239 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:06:52 +0000 Subject: [PATCH 02/58] chore(deps): update actions/setup-dotnet digest to baa11fb (#684) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index 4ba1698c..34217081 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -52,7 +52,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d92c7a84..8d4ea0f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json @@ -73,7 +73,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index c221b591..3234c041 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index e4326e04..313230e7 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fa02626e..196a2dc3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fe639f3..cc14db04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 with: global-json-file: global.json From 3d5ce76da2cb76915b326d320b1b1f3545cb6362 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:07:09 +0000 Subject: [PATCH 03/58] chore(deps): update github/codeql-action digest to cdefb33 (#685) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index af80cac1..4f9af0e4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4 + uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4 + uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4 + uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 From bf102f4a7a9b3b495b6e13d3fa575a34f6a9a32c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:07:38 +0000 Subject: [PATCH 04/58] chore(deps): update dependency reqnroll.xunit to 3.3.2 (#686) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bbdb9186..90dd8322 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,7 +43,7 @@ - + From 0847724f75ec9995d5204515e0f5d9276c6c8562 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:03:38 +0000 Subject: [PATCH 05/58] chore(deps): update spec digest to df30ead (#687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 400fa303..df30ead6 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 400fa3038d4469f0cbf77af3e79fedda406afc21 +Subproject commit df30ead675391ddbcd388a015aae6b40447ac420 From c5a95543b20e751a46949f85e4bbed6aa9984e9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:04:38 +0000 Subject: [PATCH 06/58] chore(deps): update actions/cache action to v5.0.2 (#688) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index 34217081..b1afd6c2 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -57,7 +57,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.nuget/packages key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d4ea0f3..3b9ec81c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} @@ -78,7 +78,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 3234c041..341b2d66 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -32,7 +32,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 196a2dc3..10abc2a5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,7 +27,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc14db04..29d9bbba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} From 4047da0069ad9ee41d9c26f3f575fb314b78e6ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:05:00 +0000 Subject: [PATCH 07/58] chore(deps): update dependency opentelemetry.instrumentation.aspnetcore to 1.15.0 (#689) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- samples/AspNetCore/Samples.AspNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index 723bbd4b..f98b1ba5 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -14,7 +14,7 @@ - + From 120d12d498037585b3a40e9a3f18e3efa25e836b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:58:16 +0000 Subject: [PATCH 08/58] chore(deps): update github/codeql-action digest to 19b2f06 (#690) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4f9af0e4..32b20b69 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 + uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 + uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 + uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4 From 097c80edc139918ce286779eb3032ec86d6b33d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:58:32 +0000 Subject: [PATCH 09/58] chore(deps): update spec digest to 1115b78 (#691) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index df30ead6..1115b785 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit df30ead675391ddbcd388a015aae6b40447ac420 +Subproject commit 1115b785d2cd08d16d63e96bdb6a0ac78a65e6ee From 4a29a23f609530877a80ba73a1c82c1b2b8ccb1f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:58:57 +0000 Subject: [PATCH 10/58] chore(deps): update opentelemetry-dotnet monorepo to 1.15.0 (#692) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- samples/AspNetCore/Samples.AspNetCore.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 90dd8322..119e21c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index f98b1ba5..aa37b6e6 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -13,9 +13,9 @@ - + - + From 2072376bfa194ba3a262a11565ca04bb727a55c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:27:53 +0000 Subject: [PATCH 11/58] chore(deps): update github/codeql-action digest to b20883b (#694) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 32b20b69..78fddef0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4 + uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4 From aed93d2279a1f9e916775b0dc9a346f19da1c703 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:28:12 +0000 Subject: [PATCH 12/58] chore(deps): update dependency reqnroll.xunit to 3.3.3 (#695) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 119e21c3..fc2457d7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,7 +43,7 @@ - + From 4401e9d178baaa940b418fcf89dc4fbb970525e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:28:27 +0000 Subject: [PATCH 13/58] chore(deps): update actions/attest-build-provenance action to v3.2.0 (#696) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29d9bbba..d8cab21a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json - name: Generate artifact attestation - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-path: "src/**/*.nupkg" From 705a64962ab284b4ab2e8ae550b9fb65547d1858 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:01:00 +0000 Subject: [PATCH 14/58] chore(deps): update actions/checkout digest to de0fac2 (#697) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index b1afd6c2..92341877 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b9ec81c..7ef1cc20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 submodules: recursive @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 341b2d66..fd9a70dd 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -22,7 +22,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 78fddef0..164382d5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 313230e7..4bfbcb2a 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup .NET SDK uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 10abc2a5..515e1e61 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -17,7 +17,7 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8cab21a..a64b815b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 From 150dad9a51667e1b0b88f8de408dfb016878ffb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:01:26 +0000 Subject: [PATCH 15/58] chore(deps): update github/codeql-action digest to 6bc82e0 (#698) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 164382d5..f34bc281 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4 + uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4 + uses: github/codeql-action/autobuild@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4 + uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4 From c78209d946c7806d9ae98230989d60dcd3ae3b38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:01:44 +0000 Subject: [PATCH 16/58] chore(deps): update actions/cache action to v5.0.3 (#699) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index 92341877..c6d8b9bb 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -57,7 +57,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.nuget/packages key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ef1cc20..60845ab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} @@ -78,7 +78,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index fd9a70dd..66b0707e 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -32,7 +32,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 515e1e61..7953e381 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,7 +27,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a64b815b..76869a40 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} From f895540d9153db2b2d216f1eb9454376c03412a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:40:22 +0000 Subject: [PATCH 17/58] ci: remove CodeQL analysis workflow file (#700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 72 --------------------------- 1 file changed, 72 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index f34bc281..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '19 5 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'csharp', 'actions' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4 From 53152c35f0733f90347a6e9f64e06be83427f0bc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:21:43 +0000 Subject: [PATCH 18/58] chore(deps): update spec digest to eefdf43 (#701) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 1115b785..eefdf439 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 1115b785d2cd08d16d63e96bdb6a0ac78a65e6ee +Subproject commit eefdf439c5a5b69ccde036c3c6959a4a6c17e08c From 29fd9c167a41cee1099aff9618d40b24ef9bdce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:04:43 +0000 Subject: [PATCH 19/58] refactor: improve code quality (#706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(tests): use 'using' declaration for CancellationTokenSource in test files Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore(EventExecutor): make _defaultClientName readonly Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore(InMemoryProvider): simplify flag initialization logic in constructor and UpdateFlagsAsync method Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore(EventExecutor): streamline event handler removal logic chore(LoggingHook): refine number value handling in logging Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/EventExecutor.cs | 10 ++++------ src/OpenFeature/Hooks/LoggingHook.cs | 4 ++-- .../Providers/Memory/InMemoryProvider.cs | 19 +++---------------- .../MultiProviderEventTests.cs | 2 +- .../ProviderExtensionsTests.cs | 5 +++-- .../OpenFeatureClientTests.cs | 2 +- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 4 ++-- 7 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index f65f41a1..8de21769 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -15,7 +15,7 @@ internal sealed partial class EventExecutor : IAsyncDisposable private readonly List _activeSubscriptions = []; /// placeholder for anonymous clients - private static Guid _defaultClientName = Guid.NewGuid(); + private static readonly Guid _defaultClientName = Guid.NewGuid(); private readonly Dictionary> _apiHandlers = []; private readonly Dictionary>> _clientHandlers = []; @@ -93,12 +93,10 @@ internal void RemoveClientHandler(string client, ProviderEventTypes type, EventH lock (this._lockObj) { - if (this._clientHandlers.TryGetValue(clientName, out var clientEventHandlers)) + if (this._clientHandlers.TryGetValue(clientName, out var clientEventHandlers) + && clientEventHandlers.TryGetValue(type, out var eventHandlers)) { - if (clientEventHandlers.TryGetValue(type, out var eventHandlers)) - { - eventHandlers.Remove(handler); - } + eventHandlers.Remove(handler); } } } diff --git a/src/OpenFeature/Hooks/LoggingHook.cs b/src/OpenFeature/Hooks/LoggingHook.cs index 77bf703f..e4e48727 100644 --- a/src/OpenFeature/Hooks/LoggingHook.cs +++ b/src/OpenFeature/Hooks/LoggingHook.cs @@ -153,11 +153,11 @@ public override string ToString() if (value.IsBoolean) return value.AsBoolean.ToString(); - if (value.IsNumber) + if (value.IsNumber && value.AsDouble != null) { // Value.AsDouble will attempt to cast other numbers to double // There is an implicit conversation for int/long to double - if (value.AsDouble != null) return value.AsDouble.ToString(); + return value.AsDouble.ToString(); } if (value.IsDateTime) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 617a2ecb..46dce917 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -26,14 +26,7 @@ public override Metadata GetMetadata() /// dictionary of Flags public InMemoryProvider(IDictionary? flags = null) { - if (flags == null) - { - this._flags = new Dictionary(); - } - else - { - this._flags = new Dictionary(flags); // shallow copy - } + this._flags = flags == null ? [] : new Dictionary(flags); // shallow copy } /// @@ -43,14 +36,8 @@ public InMemoryProvider(IDictionary? flags = null) public async Task UpdateFlagsAsync(IDictionary? flags = null) { var changed = this._flags.Keys.ToList(); - if (flags == null) - { - this._flags = new Dictionary(); - } - else - { - this._flags = new Dictionary(flags); // shallow copy - } + this._flags = flags == null ? [] : new Dictionary(flags); // shallow copy + changed.AddRange(this._flags.Keys.ToList()); var @event = new ProviderEventPayload { diff --git a/test/OpenFeature.Providers.MultiProvider.Tests/MultiProviderEventTests.cs b/test/OpenFeature.Providers.MultiProvider.Tests/MultiProviderEventTests.cs index d41e91cf..bc792dd5 100644 --- a/test/OpenFeature.Providers.MultiProvider.Tests/MultiProviderEventTests.cs +++ b/test/OpenFeature.Providers.MultiProvider.Tests/MultiProviderEventTests.cs @@ -319,7 +319,7 @@ private static async Task EmitEventToProvider(FeatureProvider provider, Provider private static async Task> ReadEvents(Channel channel, int expectedCount = 1, int timeoutMs = 1000) { var events = new List(); - var cts = new CancellationTokenSource(timeoutMs); + using var cts = new CancellationTokenSource(timeoutMs); try { diff --git a/test/OpenFeature.Providers.MultiProvider.Tests/ProviderExtensionsTests.cs b/test/OpenFeature.Providers.MultiProvider.Tests/ProviderExtensionsTests.cs index afb14fe6..dcf1dfe9 100644 --- a/test/OpenFeature.Providers.MultiProvider.Tests/ProviderExtensionsTests.cs +++ b/test/OpenFeature.Providers.MultiProvider.Tests/ProviderExtensionsTests.cs @@ -220,7 +220,8 @@ public async Task EvaluateAsync_WithCancellationToken_PassesToProvider() const string resolvedValue = "resolved"; var expectedDetails = new ResolutionDetails(TestFlagKey, resolvedValue, ErrorType.None, Reason.Static, TestVariant); var providerContext = new StrategyPerProviderContext(this._mockProvider, TestProviderName, ProviderStatus.Ready, TestFlagKey); - var customCancellationToken = new CancellationTokenSource().Token; + using var cts = new CancellationTokenSource(); + var customCancellationToken = cts.Token; this._mockProvider.ResolveStringValueAsync(TestFlagKey, defaultValue, this._evaluationContext, customCancellationToken) .Returns(expectedDetails); @@ -283,7 +284,7 @@ public async Task EvaluateAsync_WhenOperationCancelled_ReturnsErrorResult() { // Arrange const bool defaultValue = false; - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var providerContext = new StrategyPerProviderContext(this._mockProvider, TestProviderName, ProviderStatus.Ready, TestFlagKey); this._mockProvider.ResolveBooleanValueAsync(TestFlagKey, defaultValue, this._evaluationContext, cancellationTokenSource.Token) diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 9ea3f0dc..17ec6c79 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -470,7 +470,7 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() var defaultString = fixture.Create(); var cancelledReason = "cancelled"; - var cts = new CancellationTokenSource(); + using var cts = new CancellationTokenSource(); var featureProviderMock = Substitute.For(); diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 22a0b17a..c83546e0 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -673,7 +673,7 @@ public async Task Successful_Resolution_Should_Pass_Cancellation_Token() { var featureProvider = Substitute.For(); var hook = Substitute.For(); - var cts = new CancellationTokenSource(); + using var cts = new CancellationTokenSource(); featureProvider.GetMetadata().Returns(new Metadata(null)); featureProvider.GetProviderHooks().Returns(ImmutableList.Empty); @@ -701,7 +701,7 @@ public async Task Failed_Resolution_Should_Pass_Cancellation_Token() var hook = Substitute.For(); var flagOptions = new FlagEvaluationOptions(hook); var exceptionToThrow = new GeneralException("Fake Exception"); - var cts = new CancellationTokenSource(); + using var cts = new CancellationTokenSource(); featureProvider.GetMetadata() .Returns(new Metadata(null)); From 33a9695dc297478379561a0b5d5e3cdc88ccb286 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:44:16 +0000 Subject: [PATCH 20/58] chore(deps): update dependency microsoft.net.test.sdk to 18.3.0 (#708) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fc2457d7..066029a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -39,7 +39,7 @@ - + From 933478175c99904dea35d5503d2e97f7538b5fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:42:55 +0000 Subject: [PATCH 21/58] ci: Simplify test coverage (#705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove coverlet.msbuild package references from test projects Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore: update code coverage command in CI and enhance contribution guidelines Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore: enhance Codecov integration for improved coverage reporting on Windows and non-Windows runners Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/code-coverage.yml | 34 +++++++++++++++++-- CONTRIBUTING.md | 6 ++++ Directory.Packages.props | 6 ++-- .../OpenFeature.E2ETests.csproj | 4 --- .../OpenFeature.Hosting.Tests.csproj | 4 --- .../OpenFeature.IntegrationTests.csproj | 4 --- ...ature.Providers.MultiProvider.Tests.csproj | 4 --- .../OpenFeature.Tests.csproj | 4 --- 8 files changed, 41 insertions(+), 25 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 66b0707e..219c3b78 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -40,11 +40,41 @@ jobs: ${{ runner.os }}-nuget- - name: Run Test - run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + run: dotnet test --verbosity normal --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + # Keep globbing on non-Windows runners (works today) + - name: Upload coverage to Codecov (non-Windows) + if: runner.os != 'Windows' + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: name: Code Coverage for ${{ matrix.os }} + files: "**/TestResults/**/coverage.opencover.xml" + disable_search: true + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} + + # On Windows, avoid passing a glob that expands into multiple args + - name: Find coverage reports (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $files = Get-ChildItem -Path "$PWD" -Recurse -Filter "coverage.opencover.xml" | ForEach-Object { $_.FullName } + if (-not $files -or $files.Count -eq 0) { + throw "No coverage.opencover.xml files were found under the repository." + } + + # Codecov accepts comma-separated file paths + $csv = ($files -join ",") + "CODECOV_FILES=$csv" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Upload coverage to Codecov (Windows) + if: runner.os == 'Windows' + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + with: + name: Code Coverage for ${{ matrix.os }} + files: ${{ env.CODECOV_FILES }} + disable_search: true fail_ci_if_error: true verbose: true token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3c6300b..d74072a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,12 @@ To run unit tests execute: dotnet test test/OpenFeature.Tests/ ``` +To run unit tests with code coverage execute: + +```bash +dotnet test test/OpenFeature.Tests/ --collect:"XPlat Code Coverage" +``` + #### E2E tests To be able to run the e2e tests, first we need to initialize the submodule. diff --git a/Directory.Packages.props b/Directory.Packages.props index 066029a6..b3e58a9c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,8 @@ 9.0.0 - + 10.0.0 @@ -35,7 +36,6 @@ - @@ -64,4 +64,4 @@ - + \ No newline at end of file diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 11120976..fac909e5 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -10,10 +10,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj b/test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj index 88251769..a25b5447 100644 --- a/test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj +++ b/test/OpenFeature.Hosting.Tests/OpenFeature.Hosting.Tests.csproj @@ -10,10 +10,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj index 35d85dfd..1dd6c64b 100644 --- a/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj +++ b/test/OpenFeature.IntegrationTests/OpenFeature.IntegrationTests.csproj @@ -9,10 +9,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.Providers.MultiProvider.Tests/OpenFeature.Providers.MultiProvider.Tests.csproj b/test/OpenFeature.Providers.MultiProvider.Tests/OpenFeature.Providers.MultiProvider.Tests.csproj index 1a7dae71..9e6c377d 100644 --- a/test/OpenFeature.Providers.MultiProvider.Tests/OpenFeature.Providers.MultiProvider.Tests.csproj +++ b/test/OpenFeature.Providers.MultiProvider.Tests/OpenFeature.Providers.MultiProvider.Tests.csproj @@ -10,10 +10,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 7a529650..78cc3295 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -10,10 +10,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - From 9ed80f09d2f03a3a252079aaa051f096ee13af0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:24:08 +0000 Subject: [PATCH 22/58] chore(deps): update dependency coverlet.collector to v8 (#702) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b3e58a9c..3d0b76b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,7 +35,7 @@ - + From 3fce05bda582734b8c28247ea2b45ccfa9f4aa0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:43:09 +0000 Subject: [PATCH 23/58] chore(deps): update actions/upload-artifact action to v7 (#711) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60845ab7..f8d15651 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: nupkgs path: src/**/*.nupkg From a1778d138f63ff5c88dfad8fe80e4108f818de73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:52:33 +0000 Subject: [PATCH 24/58] ci: Change to attest action and fix SBOM generator path (#713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update attest action to v4.1.0 in SBOM generator Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore(deps): update attest action to v4.1.0 in release workflow Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * chore: update comment for attestations permission in release workflow Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * fix: correct subject-path for package attestation in SBOM generator Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/actions/sbom-generator/action.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/sbom-generator/action.yml b/.github/actions/sbom-generator/action.yml index 9d27d486..e1093aa8 100644 --- a/.github/actions/sbom-generator/action.yml +++ b/.github/actions/sbom-generator/action.yml @@ -35,7 +35,7 @@ runs: gh release upload ${{ inputs.release-tag }} ./artifacts/sboms/${{ inputs.project-name }}.bom.json - name: Attest package - uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: - subject-path: src/**/${{ inputs.project-name }}.*.nupkg + subject-path: src/${{ inputs.project-name }}/**/${{ inputs.project-name }}.*.nupkg sbom-path: ./artifacts/sboms/${{ inputs.project-name }}.bom.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76869a40..ed782054 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: permissions: id-token: write # enable GitHub OIDC token issuance for this job (NuGet login) contents: write # for SBOM release - attestations: write # for actions/attest-sbom to create attestation + attestations: write # for actions/attest to create attestation packages: read # for internal nuget reading if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }} @@ -73,7 +73,7 @@ jobs: run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json - name: Generate artifact attestation - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: "src/**/*.nupkg" From c07b17347a7dce9e305c7e5195613d112d45b80a Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:22:43 +0000 Subject: [PATCH 25/58] feat: Add IsEmpty property on ImmutableMetadata class (#714) * Add IsEmpty property on ImmutableMetadata class Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Address Gemini code review comments Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --- src/OpenFeature/Model/ImmutableMetadata.cs | 8 ++++ .../ImmutableMetadataTest.cs | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/OpenFeature/Model/ImmutableMetadata.cs b/src/OpenFeature/Model/ImmutableMetadata.cs index 7295d8b5..aec0c541 100644 --- a/src/OpenFeature/Model/ImmutableMetadata.cs +++ b/src/OpenFeature/Model/ImmutableMetadata.cs @@ -11,6 +11,14 @@ public sealed class ImmutableMetadata { private readonly ImmutableDictionary _metadata; + /// + /// Gets a value indicating whether this instance is empty. + /// + /// + /// true if this instance is empty; otherwise, false. + /// + public bool IsEmpty => this._metadata.Count == 0; + /// /// Constructor for the class. /// diff --git a/test/OpenFeature.Tests/ImmutableMetadataTest.cs b/test/OpenFeature.Tests/ImmutableMetadataTest.cs index 7f3cf3b4..673b6dde 100644 --- a/test/OpenFeature.Tests/ImmutableMetadataTest.cs +++ b/test/OpenFeature.Tests/ImmutableMetadataTest.cs @@ -315,4 +315,48 @@ public void Count_ShouldReturnCountOfMetadata() // Assert Assert.Equal(metadata.Count, result); } + + [Fact] + public void IsEmpty_ShouldReturnTrue() + { + // Arrange + var flagMetadata = new ImmutableMetadata(); + + // Act + var result = flagMetadata.IsEmpty; + + // Assert + Assert.True(result); + } + + [Fact] + public void IsEmpty_ShouldReturnFalse() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", new object() + }, + { + "stringKey", "11" + }, + { + "doubleKey", 1.2 + }, + { + "intKey", 1 + }, + { + "boolKey", true + } + }; + var flagMetadata = new ImmutableMetadata(metadata); + + // Act + var result = flagMetadata.IsEmpty; + + // Assert + Assert.False(result); + } } From a1a11dc4070cc3e26b2ea4279f76c8b7a17e831e Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:21:53 +0000 Subject: [PATCH 26/58] feat: Add public Count property on ImmutableMetadata (#715) * Add public Count property on ImmutableMetadata Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Address code review comment Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --- src/OpenFeature/Model/ImmutableMetadata.cs | 7 +++++-- .../ImmutableMetadataTest.cs | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature/Model/ImmutableMetadata.cs b/src/OpenFeature/Model/ImmutableMetadata.cs index aec0c541..cfa682fb 100644 --- a/src/OpenFeature/Model/ImmutableMetadata.cs +++ b/src/OpenFeature/Model/ImmutableMetadata.cs @@ -19,6 +19,11 @@ public sealed class ImmutableMetadata /// public bool IsEmpty => this._metadata.Count == 0; + /// + /// Gets the number of elements in this collection. + /// + public int Count => this._metadata.Count; + /// /// Constructor for the class. /// @@ -104,6 +109,4 @@ public ImmutableMetadata(Dictionary metadata) return value is T tValue ? tValue : null; } - - internal int Count => this._metadata.Count; } diff --git a/test/OpenFeature.Tests/ImmutableMetadataTest.cs b/test/OpenFeature.Tests/ImmutableMetadataTest.cs index 673b6dde..1cb700b9 100644 --- a/test/OpenFeature.Tests/ImmutableMetadataTest.cs +++ b/test/OpenFeature.Tests/ImmutableMetadataTest.cs @@ -334,7 +334,7 @@ public void IsEmpty_ShouldReturnFalse() { // Arrange var metadata = new Dictionary - { + { { "wrongKey", new object() }, @@ -359,4 +359,21 @@ public void IsEmpty_ShouldReturnFalse() // Assert Assert.False(result); } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(1000)] + public void Count_ShouldReturnCorrectNumber(int numberOfItems) + { + var metadata = Enumerable.Range(0, numberOfItems) + .ToDictionary(i => $"key{i}", i => (object)$"value{i}"); + var flagMetadata = new ImmutableMetadata(metadata); + + // Act + var count = flagMetadata.Count; + + // Assert + Assert.Equal(numberOfItems, count); + } } From ae9db552277e8a7c4ab9732bf669f72ddd24e6f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:02:42 +0000 Subject: [PATCH 27/58] chore(deps): update actions/setup-dotnet digest to c2fa09f (#716) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index c6d8b9bb..9b198204 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -52,7 +52,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8d15651..2641cd6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json @@ -73,7 +73,7 @@ jobs: submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 219c3b78..1c62c7d5 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 4bfbcb2a..8783291b 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7953e381..f7cb9334 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed782054..16469783 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: global-json-file: global.json From 58664e8ecea55e50242efe54a26bb6c9987419cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:03:00 +0000 Subject: [PATCH 28/58] chore(deps): update actions/cache action to v5.0.4 (#717) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index 9b198204..8c30c129 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -57,7 +57,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.nuget/packages key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2641cd6e..fce5f718 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} @@ -78,7 +78,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 1c62c7d5..da90e626 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -32,7 +32,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f7cb9334..027976c5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,7 +27,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16469783..6abaf09e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} From 30c4cf739008c7b28517cc55bbffb2aa2fd84344 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:03:14 +0000 Subject: [PATCH 29/58] chore(deps): update codecov/codecov-action action to v5.5.3 (#718) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/code-coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index da90e626..21cbdb9c 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -45,7 +45,7 @@ jobs: # Keep globbing on non-Windows runners (works today) - name: Upload coverage to Codecov (non-Windows) if: runner.os != 'Windows' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: name: Code Coverage for ${{ matrix.os }} files: "**/TestResults/**/coverage.opencover.xml" @@ -70,7 +70,7 @@ jobs: - name: Upload coverage to Codecov (Windows) if: runner.os == 'Windows' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: name: Code Coverage for ${{ matrix.os }} files: ${{ env.CODECOV_FILES }} From 274c7fffe355df3b89b07d82e5322eda992b1562 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:03:35 +0000 Subject: [PATCH 30/58] chore(deps): update dependency coverlet.collector to 8.0.1 (#719) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d0b76b0..c5885aff 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,7 +35,7 @@ - + From e8289eefe6681019d9943ccc6351a05604cf6057 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:03:49 +0000 Subject: [PATCH 31/58] chore(deps): update dependency opentelemetry.instrumentation.aspnetcore to 1.15.1 (#720) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- samples/AspNetCore/Samples.AspNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index aa37b6e6..cd5bee32 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -14,7 +14,7 @@ - + From 3b7745102d0ab617be0eddcc226e109d198e07cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:04:03 +0000 Subject: [PATCH 32/58] chore(deps): update marocchino/sticky-pull-request-comment action to v3 (#721) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 2aad982e..ce2a5e0f 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 + - uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3 # When the previous steps fails, the workflow would stop. By adding this # condition you can continue the execution with the populated error message. if: always() && (steps.lint_pr_title.outputs.error_message != null) @@ -36,7 +36,7 @@ jobs: ``` # Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 + uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3 with: header: pr-title-lint-error delete: true From a80e74967f26f16856e09754280df51d730bbc36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:17:37 +0000 Subject: [PATCH 33/58] chore(deps): update dependency reqnroll.xunit to 3.3.4 (#725) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c5885aff..5774aabb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,7 +43,7 @@ - + From 706fe4dfb2e1c403b9bc9c16f3046b2a897eb4bc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:17:40 +0000 Subject: [PATCH 34/58] chore(deps): update codecov/codecov-action action to v6 (#727) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/code-coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 21cbdb9c..9f67f254 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -45,7 +45,7 @@ jobs: # Keep globbing on non-Windows runners (works today) - name: Upload coverage to Codecov (non-Windows) if: runner.os != 'Windows' - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: name: Code Coverage for ${{ matrix.os }} files: "**/TestResults/**/coverage.opencover.xml" @@ -70,7 +70,7 @@ jobs: - name: Upload coverage to Codecov (Windows) if: runner.os == 'Windows' - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: name: Code Coverage for ${{ matrix.os }} files: ${{ env.CODECOV_FILES }} From e1157c29d2873a41f7ad2c2e60571a6d700b2a13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:18:16 +0000 Subject: [PATCH 35/58] chore(deps): update dependency githubactionstestlogger to 3.0.2 (#724) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5774aabb..3d5239f3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,7 @@ - + From 8a0ed59d56bbed52cd9eef29324b0ba95d80ac6d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:25:10 +0000 Subject: [PATCH 36/58] chore(deps): update opentelemetry-dotnet monorepo to 1.15.1 (#726) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- samples/AspNetCore/Samples.AspNetCore.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d5239f3..66283a36 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index cd5bee32..a0ad4b2c 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -13,9 +13,9 @@ - + - + From d83a107cbaf95fd281e6963d08597d631ac418b3 Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:05:54 +0000 Subject: [PATCH 37/58] docs: Add .NET 10 Console sample application (#722) * Add .NET 10 Console sample application Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Address Gemini code review comments Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Convert Console app to single file app Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --- OpenFeature.slnx | 3 +++ README.md | 7 +++--- samples/Console/README.md | 45 ++++++++++++++++++++++++++++++++++++++ samples/Console/app.cs | 46 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 samples/Console/README.md create mode 100644 samples/Console/app.cs diff --git a/OpenFeature.slnx b/OpenFeature.slnx index db8f4002..e3f02767 100644 --- a/OpenFeature.slnx +++ b/OpenFeature.slnx @@ -47,6 +47,9 @@ + + + diff --git a/README.md b/README.md index 45acd8d8..326b43b6 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,10 @@ public async Task Example() The [`samples/`](./samples) folder contains example applications demonstrating how to use OpenFeature in different .NET scenarios. -| Sample Name | Description | -| ------------------------------------------- | ----------------------------------------- | -| [AspNetCore](/samples/AspNetCore/README.md) | Feature flags in an ASP.NET Core Web API. | +| Sample Name | Description | +| ----------------------------------------------------- | ----------------------------------------- | +| [AspNetCore](/samples/AspNetCore/README.md) | Feature flags in an ASP.NET Core Web API. | +| [Console](/samples/Console/README.md) | Feature flags in a .NET console app. | **Getting Started with a Sample:** diff --git a/samples/Console/README.md b/samples/Console/README.md new file mode 100644 index 00000000..9470a776 --- /dev/null +++ b/samples/Console/README.md @@ -0,0 +1,45 @@ +# OpenFeature .NET Console Sample + +This sample demonstrates how to use the OpenFeature .NET SDK in a console application. It includes a simple .NET 10 console app that defines several feature flags using the `InMemoryProvider` and evaluates them across all supported flag types: `bool`, `int`, `string`, `double`, and `object`. + +The sample can easily be extended with alternative providers, which you can find in the [dotnet-sdk-contrib](https://github.com/open-feature/dotnet-sdk-contrib) repository. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) installed on your machine. + +## Setup + +1. Clone the repository: + + ```shell + git clone https://github.com/open-feature/dotnet-sdk.git openfeature-dotnet-sdk + ``` + +1. Navigate to the Console sample project directory: + + ```shell + cd openfeature-dotnet-sdk/samples/Console + ``` + +1. Run the following command to start the application: + + ```shell + dotnet run app.cs + ``` + +## Feature Flags + +The sample defines the following flags using the `InMemoryProvider`: + +| Flag Key | Type | Variants | Default Variant | +| -------------- | -------- | ----------------------------------------------------------------- | --------------- | +| `bool-flag` | `bool` | `on` → `true`, `off` → `false` | `on` | +| `numeric-flag` | `int` | `one` → `1`, `two` → `2` | `one` | +| `string-flag` | `string` | `greeting` → `"Hello, World!"`, `farewell` → `"Goodbye, World!"` | `greeting` | +| `float-flag` | `double` | `pi` → `3.14159`, `euler` → `0.577215` | `pi` | +| `object-flag` | `object` | `user1` → `"Ralph"`, `user2` → `"Lewis"` | `user2` | + +## NativeAOT + +This sample is published with [NativeAOT](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) enabled (`PublishAot=true`), demonstrating that the OpenFeature .NET SDK is fully compatible with NativeAOT compilation. See the [AOT Compatibility Guide](../../../docs/AOT_COMPATIBILITY.md) for more details. diff --git a/samples/Console/app.cs b/samples/Console/app.cs new file mode 100644 index 00000000..9f5390d3 --- /dev/null +++ b/samples/Console/app.cs @@ -0,0 +1,46 @@ +#:project ../../src/OpenFeature/OpenFeature.csproj +#:property PublishAot=true + +using OpenFeature; +using OpenFeature.Model; +using OpenFeature.Providers.Memory; + +var flags = new Dictionary +{ + { "bool-flag", new Flag(new Dictionary { { "on", true }, { "off", false } }, defaultVariant: "on") }, + { "numeric-flag", new Flag(new Dictionary { { "one", 1 }, { "two", 2 } }, defaultVariant: "one") }, + { "string-flag", new Flag(new Dictionary { { "greeting", "Hello, World!" }, { "farewell", "Goodbye, World!" } }, defaultVariant: "greeting") }, + { "float-flag", new Flag(new Dictionary { { "pi", 3.14159 }, { "euler", 0.577215 } }, defaultVariant: "pi") }, + { "object-flag", new Flag(new Dictionary { { "user1", new Value("Ralph") }, { "user2", new Value("Lewis") } }, defaultVariant: "user2" ) } +}; + +await Api.Instance.SetProviderAsync(new InMemoryProvider(flags)); + +IFeatureClient client = Api.Instance.GetClient(); + +// Evaluate the `bool-flag` flag and print the result to the console +var helloWorldResult = await client.GetBooleanValueAsync("bool-flag", false); +if (helloWorldResult) +{ + Console.WriteLine("The `bool-flag` flag was enabled!"); +} +else +{ + Console.WriteLine("The `bool-flag` flag was disabled!"); +} + +// Evaluate the `numeric-flag` flag and print the result to the console +var numericResult = await client.GetIntegerValueAsync("numeric-flag", 0); +Console.WriteLine("The `numeric-flag` flag returned {0}", numericResult); + +// Evaluate the `string-flag` flag and print the result to the console +var stringResult = await client.GetStringValueAsync("string-flag", "default"); +Console.WriteLine("The `string-flag` flag returned {0}", stringResult); + +// Evaluate the `float-flag` flag and print the result to the console +var floatResult = await client.GetDoubleValueAsync("float-flag", 0.0); +Console.WriteLine("The `float-flag` flag returned {0}", floatResult); + +// Evaluate the `object-flag` flag and print the result to the console +var objectResult = await client.GetObjectValueAsync("object-flag", new Value("Ben")); +Console.WriteLine("The `object-flag` flag returned {0}", objectResult.AsString); From a00fa901f1ef42642995c77b8b86e86f76ce876d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:11:02 +0100 Subject: [PATCH 38/58] chore(deps): update dependency githubactionstestlogger to 3.0.3 (#728) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 66283a36..99de993a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,7 @@ - + From 807cc96b3ad5a6580136ec94c46143fab7aa3c26 Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:20:45 -0400 Subject: [PATCH 39/58] chore(main): release 2.12.0 (#707) Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f58b956e..7a48e52a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.11.1" + ".": "2.12.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 64554313..ec858651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.12.0](https://github.com/open-feature/dotnet-sdk/compare/v2.11.1...v2.12.0) (2026-04-06) + + +### ✨ New Features + +* Add IsEmpty property on ImmutableMetadata class ([#714](https://github.com/open-feature/dotnet-sdk/issues/714)) ([c07b173](https://github.com/open-feature/dotnet-sdk/commit/c07b17347a7dce9e305c7e5195613d112d45b80a)) +* Add public Count property on ImmutableMetadata ([#715](https://github.com/open-feature/dotnet-sdk/issues/715)) ([a1a11dc](https://github.com/open-feature/dotnet-sdk/commit/a1a11dc4070cc3e26b2ea4279f76c8b7a17e831e)) + + +### 🔧 Refactoring + +* improve code quality ([#706](https://github.com/open-feature/dotnet-sdk/issues/706)) ([29fd9c1](https://github.com/open-feature/dotnet-sdk/commit/29fd9c167a41cee1099aff9618d40b24ef9bdce6)) + ## [2.11.1](https://github.com/open-feature/dotnet-sdk/compare/v2.11.0...v2.11.1) (2025-12-30) diff --git a/README.md b/README.md index 326b43b6..981d29e0 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.8.0) [ -![Release](https://img.shields.io/static/v1?label=release&message=v2.11.1&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.11.1) +![Release](https://img.shields.io/static/v1?label=release&message=v2.12.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.12.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index 43de66a6..7dc19f15 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.11.1 + 2.12.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index 6ceb272e..d8b69897 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.11.1 +2.12.0 From 5a4f40d26dcedb0d3b8941b2ab0f3b7fc8caca18 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:43:10 +0100 Subject: [PATCH 40/58] chore(deps): update spec digest to 8991877 (#735) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index eefdf439..89918770 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit eefdf439c5a5b69ccde036c3c6959a4a6c17e08c +Subproject commit 89918770be36d6ab1f5e498c4e4f1a1149e8ba1c From ef6e2748082da3ed18b590c0e5f8b6d365059efb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:43:26 +0100 Subject: [PATCH 41/58] chore(deps): update actions/upload-artifact action to v7.0.1 (#736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fce5f718..8fd7cfbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: nupkgs path: src/**/*.nupkg From 5fb8aa25ab4b21402c3a8879906a635717ca2d81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:43:45 +0000 Subject: [PATCH 42/58] chore(deps): update opentelemetry-dotnet monorepo to 1.15.2 (#737) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- samples/AspNetCore/Samples.AspNetCore.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 99de993a..039a28f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index a0ad4b2c..370e5874 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -13,9 +13,9 @@ - + - + From 98c394fe17d44101c16c09174c14c26fc217033e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:43:59 +0000 Subject: [PATCH 43/58] chore(deps): update dependency microsoft.net.test.sdk to 18.4.0 (#738) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 039a28f2..4fc7c3e0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -39,7 +39,7 @@ - + From 178f8369c224038d329b775d6edc22a3fce97240 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Thu, 16 Apr 2026 09:24:09 -0700 Subject: [PATCH 44/58] fix: fix 'occured' -> 'occurred' in XML doc comment (#743) ErrorType: fix 'occured' -> 'occurred' in XML doc comment XML doc comment in src/OpenFeature/Constant/ErrorType.cs line 12 read 'no error occured'. Fixed to 'occurred'. Comment-only change. Signed-off-by: SAY-5 Co-authored-by: SAY-5 --- src/OpenFeature/Constant/ErrorType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs index d36f3d96..e66cad47 100644 --- a/src/OpenFeature/Constant/ErrorType.cs +++ b/src/OpenFeature/Constant/ErrorType.cs @@ -9,7 +9,7 @@ namespace OpenFeature.Constant; public enum ErrorType { /// - /// Default value, no error occured + /// Default value, no error occurred /// None, From dad175dd353e2a733a0473b644633ce571ccb21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:43:42 +0100 Subject: [PATCH 45/58] fix: add missing variant dimension in MetricsHook metrics (#742) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add variant handling in MetricsHook and corresponding tests Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Hooks/MetricsHook.cs | 5 ++++ .../Hooks/MetricsHookTests.cs | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/OpenFeature/Hooks/MetricsHook.cs b/src/OpenFeature/Hooks/MetricsHook.cs index ae7b6e6a..430e4247 100644 --- a/src/OpenFeature/Hooks/MetricsHook.cs +++ b/src/OpenFeature/Hooks/MetricsHook.cs @@ -68,6 +68,11 @@ public override ValueTask AfterAsync(HookContext context, FlagEvaluationDe { TelemetryConstants.Reason, details.Reason ?? Reason.Unknown.ToString() } }; + if (details.Variant != null) + { + tagList.Add(TelemetryConstants.Variant, details.Variant); + } + this.AddCustomDimensions(ref tagList); this.AddFlagMetadataDimensions(details.FlagMetadata, ref tagList); diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index 16f39125..de458a4b 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -32,6 +32,35 @@ await metricsHook.AfterAsync(ctx, Assert.Equal("my-flag", measurements.Tags["feature_flag.key"]); Assert.Equal("my-provider", measurements.Tags["feature_flag.provider.name"]); Assert.Equal("STATIC", measurements.Tags["feature_flag.result.reason"]); + Assert.Equal("default", measurements.Tags["feature_flag.result.variant"]); + } + + [Fact] + public async Task Without_Variant_After_Test_Does_Not_Include_Variant() + { + // Arrange + var metricsHook = new MetricsHook(); + + using var collector = new MetricCollector(metricsHook._evaluationSuccessCounter); + + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + await metricsHook.AfterAsync(ctx, + new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", variant: null), + new Dictionary()).ConfigureAwait(true); + + var measurements = collector.LastMeasurement; + + // Assert + Assert.NotNull(measurements); + + Assert.Equal("my-flag", measurements.Tags["feature_flag.key"]); + Assert.Equal("my-provider", measurements.Tags["feature_flag.provider.name"]); + Assert.Equal("STATIC", measurements.Tags["feature_flag.result.reason"]); + Assert.DoesNotContain("feature_flag.result.variant", measurements.Tags.Keys); } [Fact] From 7d89d55d3115148bcdcb8c34c11c78eb7add1d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:08:03 +0100 Subject: [PATCH 46/58] chore(deps): Upgrade sticky-pull-request-comment to v3.0.4 (#739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated sticky-pull-request-comment action version. Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/lint-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index ce2a5e0f..fa6bde53 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3 + - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 # When the previous steps fails, the workflow would stop. By adding this # condition you can continue the execution with the populated error message. if: always() && (steps.lint_pr_title.outputs.error_message != null) @@ -36,7 +36,7 @@ jobs: ``` # Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3 + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 with: header: pr-title-lint-error delete: true From 34069d39127d1435bbdc29d8190d3e03fd73e1cf Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 21 Apr 2026 02:09:00 -0400 Subject: [PATCH 47/58] docs: fix inaccuracies in README code examples (#745) * docs: fix inaccuracies in README code examples Signed-off-by: Jonathan Norris * docs: convert README Main examples to async Task Main with await Address review feedback from @askpt: use idiomatic async/await in the TraceEnricherHook and MetricsHook examples instead of sync-over-async (.GetAwaiter().GetResult()). Signed-off-by: Jonathan Norris --------- Signed-off-by: Jonathan Norris --- README.md | 53 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 981d29e0..d944390e 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ builder = EvaluationContext.Builder(); builder.Set("region", "us-east-1"); EvaluationContext reqCtx = builder.Build(); -bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); +bool flagValue = await client.GetBooleanValueAsync("some-flag", false, reqCtx); ``` @@ -300,7 +300,7 @@ For example, a flag enhancing the appearance of a UI component might drive user ```csharp var client = Api.Instance.GetClient(); -client.Track("visited-promo-page", trackingEventDetails: new TrackingEventDetailsBuilder().SetValue(99.77).Set("currency", "USD").Build()); +client.Track("visited-promo-page", trackingEventDetails: TrackingEventDetails.Builder().SetValue(99.77).Set("currency", "USD").Build()); ``` Note that some providers may not support tracking; check the documentation for your provider for more information. @@ -364,7 +364,7 @@ public class MyProvider : FeatureProvider // resolve a string flag value } - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { // resolve an int flag value } @@ -391,25 +391,30 @@ To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need ```csharp public class MyHook : Hook { - public ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary hints = null) + public override ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { // code to run before flag evaluation } - public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null) + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { // code to run after successful flag evaluation } - public ValueTask ErrorAsync(HookContext context, Exception error, - IReadOnlyDictionary? hints = null) + public override ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { // code to run if there's an error during before hooks or during flag evaluation } - public ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails evaluationDetails, IReadOnlyDictionary hints = null) + public override ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails evaluationDetails, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { // code to run after all other stages, regardless of success/failure } @@ -421,15 +426,17 @@ Hooks support passing per-evaluation data between that stages using `hook data`. ```csharp class TimingHook : Hook { - public ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary? hints = null) + public override ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { context.Data.Set("beforeTime", DateTime.Now); return ValueTask.FromResult(context.EvaluationContext); } - public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary? hints = null) + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { var beforeTime = context.Data.Get("beforeTime") as DateTime?; var duration = DateTime.Now - beforeTime; @@ -656,6 +663,7 @@ Below are the tags added to the trace event: The following example demonstrates the use of the `TraceEnricherHook` with the `OpenFeature dotnet-sdk`. The traces are sent to a `jaeger` OTLP collector running at `localhost:4317`. ```csharp +using System.Threading.Tasks; using OpenFeature.Contrib.Providers.Flagd; using OpenFeature.Hooks; using OpenTelemetry.Exporter; @@ -666,7 +674,7 @@ using OpenTelemetry.Trace; namespace OpenFeatureTestApp { class Hello { - static void Main(string[] args) { + static async Task Main(string[] args) { // set up the OpenTelemetry OTLP exporter var tracerProvider = Sdk.CreateTracerProviderBuilder() @@ -684,14 +692,14 @@ namespace OpenFeatureTestApp var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013")); // Set the flagdProvider as the provider for the OpenFeature SDK - OpenFeature.Api.Instance.SetProvider(flagdProvider); + await OpenFeature.Api.Instance.SetProviderAsync(flagdProvider); var client = OpenFeature.Api.Instance.GetClient("my-app"); - var val = client.GetBooleanValueAsync("myBoolFlag", false, null); + var val = await client.GetBooleanValueAsync("myBoolFlag", false); // Print the value of the 'myBoolFlag' feature flag - System.Console.WriteLine(val.Result.ToString()); + System.Console.WriteLine(val); } } } @@ -738,6 +746,7 @@ Consider the following code example for usage. The following example demonstrates the use of the `MetricsHook` with the `OpenFeature dotnet-sdk`. The metrics are sent to the `console`. ```csharp +using System.Threading.Tasks; using OpenFeature.Contrib.Providers.Flagd; using OpenFeature; using OpenFeature.Hooks; @@ -747,7 +756,7 @@ using OpenTelemetry.Metrics; namespace OpenFeatureTestApp { class Hello { - static void Main(string[] args) { + static async Task Main(string[] args) { // set up the OpenTelemetry OTLP exporter var meterProvider = Sdk.CreateMeterProviderBuilder() @@ -762,14 +771,14 @@ namespace OpenFeatureTestApp var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013")); // Set the flagdProvider as the provider for the OpenFeature SDK - OpenFeature.Api.Instance.SetProvider(flagdProvider); + await OpenFeature.Api.Instance.SetProviderAsync(flagdProvider); var client = OpenFeature.Api.Instance.GetClient("my-app"); - var val = client.GetBooleanValueAsync("myBoolFlag", false, null); + var val = await client.GetBooleanValueAsync("myBoolFlag", false); // Print the value of the 'myBoolFlag' feature flag - System.Console.WriteLine(val.Result.ToString()); + System.Console.WriteLine(val); } } } From ec94acf183faa0cf74c58aa6509d0fbc5c4b8d13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:52:26 +0100 Subject: [PATCH 48/58] fix(security): update dependency opentelemetry.exporter.opentelemetryprotocol to 1.15.3 [security] (#746) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- samples/AspNetCore/Samples.AspNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index 370e5874..38371dc7 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -15,7 +15,7 @@ - + From 2b8c1f8590b13e094bbe307adda8b971dfb52cc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:24:37 +0100 Subject: [PATCH 49/58] chore(deps): update opentelemetry-dotnet monorepo to 1.15.3 (#751) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- samples/AspNetCore/Samples.AspNetCore.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4fc7c3e0..791b4f8e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index 38371dc7..47542d83 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -13,7 +13,7 @@ - + From 7d8a3ecff150fd82566d2460e0fcdf2981756b76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:45:30 +0100 Subject: [PATCH 50/58] chore(deps): update dependency opentelemetry.instrumentation.aspnetcore to 1.15.2 (#750) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- samples/AspNetCore/Samples.AspNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj index 47542d83..0f742ca3 100644 --- a/samples/AspNetCore/Samples.AspNetCore.csproj +++ b/samples/AspNetCore/Samples.AspNetCore.csproj @@ -14,7 +14,7 @@ - + From 96c6c4246157a17f0614693e0263e4a6ee012fe5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:45:48 +0100 Subject: [PATCH 51/58] chore(deps): update actions/cache action to v5.0.5 (#749) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/aot-compatibility.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/aot-compatibility.yml index 8c30c129..01d8fd30 100644 --- a/.github/workflows/aot-compatibility.yml +++ b/.github/workflows/aot-compatibility.yml @@ -57,7 +57,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fd7cfbf..db1b0184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} @@ -78,7 +78,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 9f67f254..5dde19b1 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -32,7 +32,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 027976c5..01d5fe66 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,7 +27,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6abaf09e..ea08e608 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: global-json-file: global.json - name: Cache NuGet packages - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} From 1e3bb401ceccbc8d3547de474831f90af4ac8f65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:46:03 +0100 Subject: [PATCH 52/58] chore(deps): update spec digest to ac62b5d (#748) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 89918770..ac62b5d5 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 89918770be36d6ab1f5e498c4e4f1a1149e8ba1c +Subproject commit ac62b5d5141e090141322dd634b882525fdb0c65 From fdab3cc8d91529478764aaff7792cd5a0e3f8dae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:47:07 +0000 Subject: [PATCH 53/58] chore(deps): update nuget/login digest to 8d19675 (#747) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea08e608..f561c647 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,7 @@ jobs: # Get a short-lived NuGet API key - name: NuGet login (OIDC → temp API key) - uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1 + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1 id: login with: user: ${{secrets.NUGET_USER}} From 6ccc2596d48e8a61c07b0c84ca88de2554d9c1bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:50:56 +0100 Subject: [PATCH 54/58] chore(deps): update dependency microsoft.net.test.sdk to 18.5.1 (#752) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 791b4f8e..ffb42d74 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -39,7 +39,7 @@ - + From c6ac1fa9b0a7d5a0eae087752f698780cbcd117c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:01:05 +0100 Subject: [PATCH 55/58] chore(deps): update dependency coverlet.collector to v10 (#753) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ffb42d74..cb22dbe7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,7 +35,7 @@ - + From 34bf89a45bb4d684abefb515d184929446fce13d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:01:23 +0100 Subject: [PATCH 56/58] chore(deps): update googleapis/release-please-action action to v5 (#754) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f561c647..ab951e87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4 + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 id: release with: token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} From 7802950d15b666f85c8582b698c913d072af23a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:07:29 +0100 Subject: [PATCH 57/58] feat: Add isolated API instance functionality (#740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add tracking for bound API instance in FeatureProvider Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: clear ownership of bound API instances during provider shutdown Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: add CreateIsolated method and provider ownership validation to Api class Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: add unit tests for isolated API instances and provider behavior Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: add demonstration of isolated API instance creation and usage Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: add isolated API instance testing functionality Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Fix formatting Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * test: enhance exception message assertions for provider binding in isolated API tests Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * refactor: improve provider instance binding logic Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * Move to a separate namespace. Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * feat: add experimental identifiers for Isolated API features and update related code Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> * fix: reorder using directives for consistency in IsolatedApiTests Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- samples/Console/README.md | 11 + samples/Console/app.cs | 29 ++ src/OpenFeature/Api.cs | 17 + .../Constant/FeatureDiagnosticCodes.cs | 38 ++ src/OpenFeature/FeatureProvider.cs | 27 ++ .../Isolated/OpenFeatureFactory.cs | 36 ++ src/OpenFeature/ProviderRepository.cs | 13 + .../OpenFeature.AotCompatibility.csproj | 5 +- test/OpenFeature.AotCompatibility/Program.cs | 39 ++ .../Isolated/IsolatedApiTests.cs | 378 ++++++++++++++++++ 10 files changed, 589 insertions(+), 4 deletions(-) create mode 100644 src/OpenFeature/Constant/FeatureDiagnosticCodes.cs create mode 100644 src/OpenFeature/Isolated/OpenFeatureFactory.cs create mode 100644 test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs diff --git a/samples/Console/README.md b/samples/Console/README.md index 9470a776..fac135d7 100644 --- a/samples/Console/README.md +++ b/samples/Console/README.md @@ -43,3 +43,14 @@ The sample defines the following flags using the `InMemoryProvider`: ## NativeAOT This sample is published with [NativeAOT](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) enabled (`PublishAot=true`), demonstrating that the OpenFeature .NET SDK is fully compatible with NativeAOT compilation. See the [AOT Compatibility Guide](../../../docs/AOT_COMPATIBILITY.md) for more details. + +## Isolated API Instance + +The sample also demonstrates how to create an **isolated API instance** using `Api.CreateIsolated()`. The isolated instance has its own provider with different flag values, proving that it operates independently from the global singleton. + +| Flag Key | Type | Variants | Default Variant | +| -------------- | -------- | ---------------------------------------------------------------------------------- | --------------- | +| `bool-flag` | `bool` | `on` → `true`, `off` → `false` | `off` | +| `string-flag` | `string` | `greeting` → `"Howdy, Isolated World!"`, `farewell` → `"See ya!"` | `greeting` | + +The isolated instance evaluates flags from its own provider, then shuts down without affecting the global singleton. This is useful for testing, multi-tenant applications, and dependency injection scenarios. See the [specification](https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances) for more details. diff --git a/samples/Console/app.cs b/samples/Console/app.cs index 9f5390d3..e780fac1 100644 --- a/samples/Console/app.cs +++ b/samples/Console/app.cs @@ -44,3 +44,32 @@ // Evaluate the `object-flag` flag and print the result to the console var objectResult = await client.GetObjectValueAsync("object-flag", new Value("Ben")); Console.WriteLine("The `object-flag` flag returned {0}", objectResult.AsString); + +// --- Isolated API Instance --- +// Create an independent API instance with its own provider and state. +// This is useful for testing, multi-tenant apps, and DI scenarios. + +var isolatedFlags = new Dictionary +{ + { "bool-flag", new Flag(new Dictionary { { "on", true }, { "off", false } }, defaultVariant: "off") }, + { "string-flag", new Flag(new Dictionary { { "greeting", "Howdy, Isolated World!" }, { "farewell", "See ya!" } }, defaultVariant: "greeting") }, +}; + +var isolated = Api.CreateIsolated(); +await isolated.SetProviderAsync(new InMemoryProvider(isolatedFlags)); + +var isolatedClient = isolated.GetClient(); + +Console.WriteLine(); +Console.WriteLine("--- Isolated API Instance ---"); + +// The isolated instance has its own flag values +var isolatedBool = await isolatedClient.GetBooleanValueAsync("bool-flag", true); +Console.WriteLine("Isolated `bool-flag`: {0} (singleton was: {1})", isolatedBool, helloWorldResult); + +var isolatedString = await isolatedClient.GetStringValueAsync("string-flag", "default"); +Console.WriteLine("Isolated `string-flag`: {0} (singleton was: {1})", isolatedString, stringResult); + +// Shut down the isolated instance — does not affect the global singleton +await isolated.ShutdownAsync(); +Console.WriteLine("Isolated instance shut down. Singleton is unaffected."); diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 3532e99e..8e4b83dd 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -56,6 +56,7 @@ public Task SetProviderAsync(FeatureProvider featureProvider) /// A that completes once Provider initialization is complete. public async Task SetProviderAsync(FeatureProvider featureProvider, CancellationToken cancellationToken) { + this.ValidateProviderOwnership(featureProvider); this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitializationAsync, this.AfterErrorAsync, cancellationToken) .ConfigureAwait(false); @@ -91,6 +92,7 @@ public async Task SetProviderAsync(string domain, FeatureProvider featureProvide { throw new ArgumentNullException(nameof(domain)); } + this.ValidateProviderOwnership(featureProvider); this._eventExecutor.RegisterClientFeatureProvider(domain, featureProvider); await this._repository.SetProviderAsync(domain, featureProvider, this.GetContext(), this.AfterInitializationAsync, this.AfterErrorAsync, cancellationToken) .ConfigureAwait(false); @@ -401,4 +403,19 @@ internal static void SetInstance(Api api) { Instance = api; } + + /// + /// Validates that the given provider is not already bound to a different API instance. + /// + /// The provider to validate ownership for. + /// Thrown if the provider is already bound to a different API instance. + private void ValidateProviderOwnership(FeatureProvider featureProvider) + { + if (!featureProvider.TryBindApiInstance(this)) + { + throw new InvalidOperationException( + "This provider instance is already bound to a different API instance. " + + "A provider should not be registered with more than one API instance simultaneously."); + } + } } diff --git a/src/OpenFeature/Constant/FeatureDiagnosticCodes.cs b/src/OpenFeature/Constant/FeatureDiagnosticCodes.cs new file mode 100644 index 00000000..dae01f60 --- /dev/null +++ b/src/OpenFeature/Constant/FeatureDiagnosticCodes.cs @@ -0,0 +1,38 @@ +namespace OpenFeature.Constant; + +/// +/// Contains identifiers for experimental features and diagnostics in the OpenFeature framework. +/// +/// +/// Experimental - This class includes identifiers that allow developers to track and conditionally enable +/// experimental features. Each identifier follows a structured code format to indicate the feature domain, +/// maturity level, and unique identifier. Note that experimental features are subject to change or removal +/// in future releases. +/// +/// Basic Information
+/// These identifiers conform to OpenFeature’s Diagnostics Specifications, allowing developers to recognize +/// and manage experimental features effectively. +///
+///
+/// +/// +/// Code Structure: +/// - "OF" - Represents the OpenFeature library. +/// - "ISO" - Indicates the Isolated API domain. +/// - "001" - Unique identifier for a specific feature. +/// +/// +internal static class FeatureDiagnosticCodes +{ + /// + /// Identifier for the experimental Isolated API features within the OpenFeature framework. + /// + /// + /// OFISO001 identifier marks experimental features in the Isolated (ISO) domain. + /// + /// Usage: + /// Developers can use this identifier to conditionally enable or test experimental Isolated API features. + /// It is part of the OpenFeature diagnostics system to help track experimental functionality. + /// + public const string IsolatedApi = "OFISO001"; +} diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 16c495c6..ddefface 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -98,6 +98,33 @@ public abstract Task> ResolveStructureValueAsync(string /// internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady; + /// + /// Tracks which Api instance this provider is currently bound to. + /// A provider should not be registered with more than one API instance simultaneously (spec 1.8.4). + /// + private Api? _boundApiInstance; + + /// + /// Attempts to bind this provider to the given API instance. + /// Uses for thread-safe check-and-set. + /// + /// The API instance to bind to. + /// true if the provider was successfully bound (or was already bound to the same instance); + /// false if the provider is already bound to a different API instance. + internal bool TryBindApiInstance(Api api) + { + var previous = Interlocked.CompareExchange(ref this._boundApiInstance, api, null); + return previous is null || ReferenceEquals(previous, api); + } + + /// + /// Clears the API instance binding, allowing this provider to be registered with another API instance. + /// + internal void UnbindApiInstance() + { + this._boundApiInstance = null; + } + /// /// /// This method is called before a provider is used to evaluate flags. Providers can overwrite this method, diff --git a/src/OpenFeature/Isolated/OpenFeatureFactory.cs b/src/OpenFeature/Isolated/OpenFeatureFactory.cs new file mode 100644 index 00000000..bcc4e13a --- /dev/null +++ b/src/OpenFeature/Isolated/OpenFeatureFactory.cs @@ -0,0 +1,36 @@ +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace OpenFeature.Isolated; + +/// +/// Factory for creating isolated instances of the OpenFeature API. +/// +/// +/// This factory provides a method to create new, independent instances of the OpenFeature API with fully isolated state. Each isolated instance maintains its own providers, evaluation context, hooks, event handlers, +/// and transaction context propagators. It does not share state with the global singleton or with any other isolated instance. +/// +public static class OpenFeatureFactory +{ + /// + /// Creates a new, independent instance of the OpenFeature API with fully isolated state. + /// + /// Each isolated instance maintains its own providers, evaluation context, hooks, event handlers, + /// and transaction context propagators. It does not share state with the global + /// singleton or with any other isolated instance. + /// + /// + /// + /// + /// A single provider instance should not be bound to more than one API instance at a time. + /// Attempting to do so will result in an . + /// + /// + /// A new, independent instance. + /// Specification 1.8 - Isolated API Instances +#if NET8_0_OR_GREATER + [Experimental(Constant.FeatureDiagnosticCodes.IsolatedApi)] +#endif + public static Api CreateIsolated() => new Api(); +} diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 5539d0ea..d6230e8d 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -193,6 +193,13 @@ private async Task ShutdownIfUnusedAsync( return; } + // Clear ownership while still under the write lock — the provider is confirmed unused. + // This prevents a race where async shutdown clears ownership after a re-registration. + if (targetProvider != null) + { + targetProvider.UnbindApiInstance(); + } + await this.SafeShutdownProviderAsync(targetProvider, cancellationToken).ConfigureAwait(false); } @@ -270,6 +277,12 @@ internal async Task ShutdownAsync(Action? afterError // Set a default provider so the Api is ready to be used again. this._defaultProvider = new NoOpFeatureProvider(); this._featureProviders.Clear(); + + // Clear ownership under the write lock for all providers being shut down. + foreach (var provider in providers) + { + provider.UnbindApiInstance(); + } } finally { diff --git a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj index bc98f8a3..f7851637 100644 --- a/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj +++ b/test/OpenFeature.AotCompatibility/OpenFeature.AotCompatibility.csproj @@ -14,6 +14,7 @@ false NU1903 OpenFeature.AotCompatibility + $(NoWarn);OFISO001 @@ -27,8 +28,4 @@ - - - - diff --git a/test/OpenFeature.AotCompatibility/Program.cs b/test/OpenFeature.AotCompatibility/Program.cs index 5529eef2..8c963365 100644 --- a/test/OpenFeature.AotCompatibility/Program.cs +++ b/test/OpenFeature.AotCompatibility/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenFeature.Constant; +using OpenFeature.Isolated; using OpenFeature.Model; using OpenFeature.Providers.MultiProvider; using OpenFeature.Providers.MultiProvider.Models; @@ -27,6 +28,9 @@ private static async Task Main(string[] args) // Test basic API functionality await TestBasicApiAsync(); + // Test isolated API instances + await TestIsolatedApiAsync(); + // Test MultiProvider AOT compatibility await TestMultiProviderAotCompatibilityAsync(); @@ -49,6 +53,41 @@ private static async Task Main(string[] args) } } + private static async Task TestIsolatedApiAsync() + { + Console.WriteLine("\nTesting isolated API instances..."); + + // Create an isolated instance + var isolated = OpenFeatureFactory.CreateIsolated(); + Console.WriteLine($"✓- Isolated API instance created: {isolated.GetType().Name}"); + + // Verify it is distinct from the singleton + if (ReferenceEquals(isolated, Api.Instance)) + { + throw new InvalidOperationException("Isolated instance should not be the same as the singleton."); + } + Console.WriteLine("✓- Isolated instance is distinct from singleton"); + + // Set a provider on the isolated instance + var provider = new TestProvider(); + await isolated.SetProviderAsync(provider); + Console.WriteLine($"✓- Provider set on isolated instance: {isolated.GetProviderMetadata()?.Name}"); + + // Create a client and evaluate a flag + var client = isolated.GetClient("isolated-client"); + var result = await client.GetBooleanValueAsync("test-flag", false); + Console.WriteLine($"✓- Isolated client flag evaluation: {result}"); + + // Set context on isolated instance and verify independence + var context = EvaluationContext.Builder().Set("scope", "isolated").Build(); + isolated.SetContext(context); + Console.WriteLine($"✓- Isolated context set with {context.Count} attributes"); + + // Shutdown the isolated instance + await isolated.ShutdownAsync(); + Console.WriteLine("✓- Isolated instance shut down successfully"); + } + private static async Task TestBasicApiAsync() { Console.WriteLine("\nTesting basic API functionality..."); diff --git a/test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs b/test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs new file mode 100644 index 00000000..ed90ac5e --- /dev/null +++ b/test/OpenFeature.Tests/Isolated/IsolatedApiTests.cs @@ -0,0 +1,378 @@ +using NSubstitute; +using OpenFeature.Constant; +using OpenFeature.Isolated; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; + +namespace OpenFeature.Tests.Isolated; + +/// +/// Tests for isolated API instances (spec section 1.8). +/// Each test creates its own isolated instances and cleans them up, +/// so no shared state fixture is needed. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental(FeatureDiagnosticCodes.IsolatedApi)] +#endif +public class IsolatedApiTests +{ + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void CreateIsolated_Should_Return_New_Instance() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + + Assert.NotNull(isolated); + Assert.NotSame(Api.Instance, isolated); + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void CreateIsolated_Should_Return_Distinct_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + + Assert.NotSame(isolated1, isolated2); + } + + [Fact] + [Specification("1.8.2", "Instances returned by the factory function MUST conform to the same API contract as the global singleton.")] + public async Task Isolated_Instance_Should_Support_Full_Api_Contract() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + // Provider management + var provider = new TestProvider(); + await isolated.SetProviderAsync(provider); + Assert.Equal(provider, isolated.GetProvider()); + Assert.Equal(provider.GetMetadata()?.Name, isolated.GetProviderMetadata()?.Name); + + // Client creation + var client = isolated.GetClient("test-client"); + Assert.NotNull(client); + Assert.Equal("test-client", client.GetMetadata().Name); + + // Hooks + var hook = Substitute.For(); + isolated.AddHooks(hook); + Assert.Contains(hook, isolated.GetHooks()); + + // Context + var context = EvaluationContext.Builder().Set("key", "value").Build(); + isolated.SetContext(context); + Assert.Equal(context, isolated.GetContext()); + + // Event handling (just verify it doesn't throw) + isolated.AddHandler(ProviderEventTypes.ProviderReady, _ => { }); + + // Transaction context + var propagator = Substitute.For(); + isolated.SetTransactionContextPropagator(propagator); + } + finally + { + await isolated.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public async Task Isolated_Provider_Should_Not_Affect_Singleton() + { + Api.ResetApi(); + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("isolated-provider"); + await isolated.SetProviderAsync(provider); + + // The singleton should still have the default NoOp provider + Assert.Equal(NoOpProvider.NoOpProviderName, Api.Instance.GetProviderMetadata()?.Name); + Assert.Equal("isolated-provider", isolated.GetProviderMetadata()?.Name); + } + finally + { + await isolated.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public async Task Isolated_Provider_Should_Not_Affect_Other_Isolated_Instance() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider1 = new TestProvider("provider-1"); + var provider2 = new TestProvider("provider-2"); + await isolated1.SetProviderAsync(provider1); + await isolated2.SetProviderAsync(provider2); + + Assert.Equal("provider-1", isolated1.GetProviderMetadata()?.Name); + Assert.Equal("provider-2", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void Isolated_Hooks_Should_Not_Affect_Other_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + + var hook1 = Substitute.For(); + var hook2 = Substitute.For(); + + isolated1.AddHooks(hook1); + isolated2.AddHooks(hook2); + + Assert.Contains(hook1, isolated1.GetHooks()); + Assert.DoesNotContain(hook2, isolated1.GetHooks()); + + Assert.Contains(hook2, isolated2.GetHooks()); + Assert.DoesNotContain(hook1, isolated2.GetHooks()); + } + + [Fact] + [Specification("1.8.1", "The API MUST expose a factory function which creates and returns a new, independent instance of the API.")] + public void Isolated_Context_Should_Not_Affect_Other_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + + var context1 = EvaluationContext.Builder().Set("instance", "one").Build(); + var context2 = EvaluationContext.Builder().Set("instance", "two").Build(); + + isolated1.SetContext(context1); + isolated2.SetContext(context2); + + Assert.Equal(context1, isolated1.GetContext()); + Assert.Equal(context2, isolated2.GetContext()); + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Same_Provider_Should_Throw_When_Bound_To_Multiple_Api_Instances() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("shared-provider"); + await isolated1.SetProviderAsync(provider); + + var exception = await Assert.ThrowsAsync( + () => isolated2.SetProviderAsync(provider)); + + Assert.Equal("This provider instance is already bound to a different API instance. " + + "A provider should not be registered with more than one API instance simultaneously.", exception.Message); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Same_Provider_Should_Throw_When_Bound_To_Multiple_Api_Instances_Domain() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("shared-provider"); + await isolated1.SetProviderAsync("domain-1", provider); + + var exception = await Assert.ThrowsAsync( + () => isolated2.SetProviderAsync("domain-2", provider)); + + Assert.Equal("This provider instance is already bound to a different API instance. " + + "A provider should not be registered with more than one API instance simultaneously.", exception.Message); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Concurrent_Binding_Should_Allow_Only_One_Api_Instance() + { + const int instanceCount = 10; + var provider = new TestProvider("contested-provider"); + var instances = Enumerable.Range(0, instanceCount).Select(_ => OpenFeatureFactory.CreateIsolated()).ToList(); + try + { + var tasks = instances.Select(api => api.SetProviderAsync(provider)).ToList(); + var results = await Task.WhenAll(tasks.Select(async t => + { + try { await t; return (Exception?)null; } + catch (InvalidOperationException ex) { return ex; } + })); + + var successes = results.Count(r => r is null); + var failures = results.Count(r => r is InvalidOperationException); + + Assert.Equal(1, successes); + Assert.Equal(instanceCount - 1, failures); + } + finally + { + foreach (var api in instances) + { + await api.ShutdownAsync(); + } + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Provider_Can_Be_Rebound_After_Shutdown() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("rebindable-provider"); + await isolated1.SetProviderAsync(provider); + + // Shut down first instance — this frees the provider + await isolated1.ShutdownAsync(); + + // Now the provider should be bindable to another instance + await isolated2.SetProviderAsync(provider); + Assert.Equal("rebindable-provider", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.4", "A provider instance SHOULD NOT be registered with more than one API instance simultaneously.")] + public async Task Same_Provider_Can_Be_Bound_To_Multiple_Domains_Within_Same_Api() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("multi-domain-provider"); + await isolated.SetProviderAsync("domain-a", provider); + await isolated.SetProviderAsync("domain-b", provider); + + Assert.Equal(provider, isolated.GetProvider("domain-a")); + Assert.Equal(provider, isolated.GetProvider("domain-b")); + } + finally + { + await isolated.ShutdownAsync(); + } + } + + [Fact] + public async Task Shutdown_Isolated_Should_Not_Affect_Singleton() + { + Api.ResetApi(); + var singletonProvider = new TestProvider("singleton-provider"); + await Api.Instance.SetProviderAsync(singletonProvider); + + var isolated = OpenFeatureFactory.CreateIsolated(); + var isolatedProvider = new TestProvider("isolated-provider"); + await isolated.SetProviderAsync(isolatedProvider); + + // Shutting down isolated instance + await isolated.ShutdownAsync(); + + // Singleton should be unaffected + Assert.Equal("singleton-provider", Api.Instance.GetProviderMetadata()?.Name); + + // Cleanup + await Api.Instance.ShutdownAsync(); + } + + [Fact] + public async Task Shutdown_Isolated_Should_Not_Affect_Other_Isolated() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var provider1 = new TestProvider("provider-1"); + var provider2 = new TestProvider("provider-2"); + await isolated1.SetProviderAsync(provider1); + await isolated2.SetProviderAsync(provider2); + + await isolated1.ShutdownAsync(); + + // isolated2 should be unaffected + Assert.Equal("provider-2", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated2.ShutdownAsync(); + } + } + + [Fact] + public async Task Provider_Can_Be_Rebound_After_Replacement() + { + var isolated1 = OpenFeatureFactory.CreateIsolated(); + var isolated2 = OpenFeatureFactory.CreateIsolated(); + try + { + var providerA = new TestProvider("provider-a"); + var providerB = new TestProvider("provider-b"); + + await isolated1.SetProviderAsync(providerA); + + // Replace providerA with providerB in isolated1 — providerA should be unbound + await isolated1.SetProviderAsync(providerB); + + // Allow some time for the async shutdown to complete + await Task.Delay(100); + + // providerA should now be free to bind to isolated2 + await isolated2.SetProviderAsync(providerA); + Assert.Equal("provider-a", isolated2.GetProviderMetadata()?.Name); + } + finally + { + await isolated1.ShutdownAsync(); + await isolated2.ShutdownAsync(); + } + } + + [Fact] + [Specification("1.8.2", "Instances returned by the factory function MUST conform to the same API contract as the global singleton.")] + public async Task Isolated_Client_Should_Evaluate_Flags() + { + var isolated = OpenFeatureFactory.CreateIsolated(); + try + { + var provider = new TestProvider("eval-provider"); + await isolated.SetProviderAsync(provider); + + var client = isolated.GetClient(); + + // TestProvider inverts boolean defaults + var result = await client.GetBooleanValueAsync("test-flag", false); + Assert.True(result); + } + finally + { + await isolated.ShutdownAsync(); + } + } +} From b3fa7f41c926a93a69ac2d599089ce4045d62770 Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:13:29 -0400 Subject: [PATCH 58/58] chore(main): release 2.13.0 (#744) Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ README.md | 4 ++-- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7a48e52a..d18e9443 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.12.0" + ".": "2.13.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index ec858651..13fd99ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [2.13.0](https://github.com/open-feature/dotnet-sdk/compare/v2.12.0...v2.13.0) (2026-04-30) + + +### 🐛 Bug Fixes + +* add missing variant dimension in MetricsHook metrics ([#742](https://github.com/open-feature/dotnet-sdk/issues/742)) ([dad175d](https://github.com/open-feature/dotnet-sdk/commit/dad175dd353e2a733a0473b644633ce571ccb21a)) +* fix 'occured' -> 'occurred' in XML doc comment ([#743](https://github.com/open-feature/dotnet-sdk/issues/743)) ([178f836](https://github.com/open-feature/dotnet-sdk/commit/178f8369c224038d329b775d6edc22a3fce97240)) +* **security:** update dependency opentelemetry.exporter.opentelemetryprotocol to 1.15.3 [security] ([#746](https://github.com/open-feature/dotnet-sdk/issues/746)) ([ec94acf](https://github.com/open-feature/dotnet-sdk/commit/ec94acf183faa0cf74c58aa6509d0fbc5c4b8d13)) + + +### ✨ New Features + +* Add isolated API instance functionality ([#740](https://github.com/open-feature/dotnet-sdk/issues/740)) ([7802950](https://github.com/open-feature/dotnet-sdk/commit/7802950d15b666f85c8582b698c913d072af23a0)) + ## [2.12.0](https://github.com/open-feature/dotnet-sdk/compare/v2.11.1...v2.12.0) (2026-04-06) diff --git a/README.md b/README.md index d944390e..e221ee07 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ [![Specification](https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.8.0) [ -![Release](https://img.shields.io/static/v1?label=release&message=v2.12.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.12.0) +![Release](https://img.shields.io/static/v1?label=release&message=v2.13.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.13.0) [![Slack](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) [![Codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) diff --git a/build/Common.prod.props b/build/Common.prod.props index 7dc19f15..29b87ac7 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 2.12.0 + 2.13.0 git https://github.com/open-feature/dotnet-sdk OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings. diff --git a/version.txt b/version.txt index d8b69897..fb2c0766 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.12.0 +2.13.0