From 963b54c65fd1d7d5abc5652e91c53de79d1dfcbf Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:31:38 +0400 Subject: [PATCH 001/123] [SIP-123] feat: Create OpenFeature.DependencyInjection project --- OpenFeature.sln | 21 ++++++++++++------- .../OpenFeature.DependencyInjection.csproj | 9 ++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj diff --git a/OpenFeature.sln b/OpenFeature.sln index 6f1cce8d..7fccd63f 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -77,7 +77,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -101,21 +103,26 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj new file mode 100644 index 00000000..edd721a3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -0,0 +1,9 @@ + + + + netstandard2.0;net6.0;net8.0;net462 + enable + enable + + + From a8034292060a058467bb1e5a5969ec46a67231bf Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:37:43 +0400 Subject: [PATCH 002/123] chore: Update TargetFrameworks to net6.0 --- .../OpenFeature.DependencyInjection.csproj | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index edd721a3..4589884a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,9 +1,14 @@ - netstandard2.0;net6.0;net8.0;net462 + net6.0; enable enable + OpenFeature + + + + From 79ee277c289d64376e948eb4bd1f3df4e9c18035 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:38:33 +0400 Subject: [PATCH 003/123] feat: Create OpenFeatureBuilder record --- .../OpenFeatureBuilder.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs new file mode 100644 index 00000000..03cad3ca --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature; + +/// +/// Describes a backed by an . +/// +/// +public sealed record OpenFeatureBuilder(IServiceCollection Services) +{ + /// + /// Indicates whether the evaluation context has been configured. + /// This property is used to determine if specific configurations or services + /// should be initialized based on the presence of an evaluation context. + /// + internal bool IsContextConfigured { get; set; } +} From 9f3c40e4a15a0938b2490956ab32d325696aa1e1 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:41:45 +0400 Subject: [PATCH 004/123] feat: Add OpenFeatureBuilderExtensions --- .../OpenFeature.DependencyInjection.csproj | 4 ++ .../OpenFeatureBuilderExtensions.cs | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 4589884a..f6af26ef 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..54e20426 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.Model; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// This method is used to add a new context to the service collection. + /// + /// + /// the desired configuration + /// + /// the instance + /// + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + return builder.AddContext((b, _) => configure(b)); + } + + /// + /// This method is used to add a new context to the service collection. + /// + /// + /// the desired configuration + /// + /// the instance + /// + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + builder.IsContextConfigured = true; + builder.Services.TryAddTransient(provider => { + var contextBuilder = EvaluationContext.Builder(); + configure(contextBuilder, provider); + return contextBuilder.Build(); + }); + + return builder; + } +} From 6786bd8d61c0a0b9e242c9bf3b21d989f0725212 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:48:35 +0400 Subject: [PATCH 005/123] feat: Add IFeatureLifecycleManager interface and implementation --- Directory.Packages.props | 1 + .../IFeatureLifecycleManager.cs | 24 ++++++++++++ .../Internal/FeatureLifecycleManager.cs | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7227000a..ed54c052 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs new file mode 100644 index 00000000..2085bda4 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs @@ -0,0 +1,24 @@ +namespace OpenFeature; + +/// +/// Defines the contract for managing the lifecycle of a feature api. +/// +public interface IFeatureLifecycleManager +{ + /// + /// Ensures that the feature provider is properly initialized and ready to be used. + /// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of initializing the feature provider. + /// Thrown when the feature provider is not registered or is in an invalid state. + ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default); + + /// + /// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved. + /// This method should handle all necessary cleanup and shutdown operations for the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of shutting down the feature provider. + ValueTask ShutdownAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs new file mode 100644 index 00000000..1b2f47c9 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature.Internal; + +[ExcludeFromCodeCoverage] +internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager +{ + private readonly Api _featureApi; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger logger) + { + _featureApi = featureApi; + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting initialization of the feature provider"); + var featureProvider = _serviceProvider.GetService(); + if (featureProvider == null) + { + throw new InvalidOperationException("Feature provider is not registered in the service collection."); + } + await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); + } + + /// + public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Shutting down the feature provider."); + await _featureApi.ShutdownAsync().ConfigureAwait(false); + } +} From f373eb044d95933609a5edabb41efe7bc728a736 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:51:20 +0400 Subject: [PATCH 006/123] feat: Add OpenFeatureServiceCollectionExtensions --- .../OpenFeatureServiceCollectionExtensions.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs new file mode 100644 index 00000000..17ffc7e8 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.Internal; +using OpenFeature.Model; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +[ExcludeFromCodeCoverage] +public static class OpenFeatureServiceCollectionExtensions +{ + /// + /// This method is used to add OpenFeature to the service collection. + /// OpenFeature will be registered as a singleton. + /// + /// + /// the desired configuration + /// the current instance + public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + services.TryAddSingleton(Api.Instance); + services.TryAddSingleton(); + + var builder = new OpenFeatureBuilder(services); + configure(builder); + + if (builder.IsContextConfigured) + { + services.TryAddScoped(static provider => { + var api = provider.GetRequiredService(); + var client = api.GetClient(); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + services.TryAddScoped(static provider => { + var api = provider.GetRequiredService(); + return api.GetClient(); + }); + } + + return services; + } +} From 1f862989c1325cb22d223569c16baa384129cac8 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 8 Oct 2024 23:16:26 +0400 Subject: [PATCH 007/123] feat: Add AddProvider extension method in OpenFeatureBuilderExtensions --- .../OpenFeatureBuilderExtensions.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 54e20426..579b61ca 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -11,11 +11,9 @@ public static partial class OpenFeatureBuilderExtensions /// /// This method is used to add a new context to the service collection. /// - /// + /// The instance. /// the desired configuration - /// - /// the instance - /// + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -29,11 +27,9 @@ public static OpenFeatureBuilder AddContext( /// /// This method is used to add a new context to the service collection. /// - /// + /// The instance. /// the desired configuration - /// - /// the instance - /// + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -42,7 +38,8 @@ public static OpenFeatureBuilder AddContext( ArgumentNullException.ThrowIfNull(configure); builder.IsContextConfigured = true; - builder.Services.TryAddTransient(provider => { + builder.Services.TryAddTransient(provider => + { var contextBuilder = EvaluationContext.Builder(); configure(contextBuilder, provider); return contextBuilder.Build(); @@ -50,4 +47,19 @@ public static OpenFeatureBuilder AddContext( return builder; } + + /// + /// Adds a feature provider to the service collection. + /// + /// The type of the feature provider, which must inherit from . + /// The instance. + /// A factory method to create the feature provider, using the service provider. + /// The instance. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory) + where T : FeatureProvider + { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.TryAddSingleton(providerFactory); + return builder; + } } From f1039a26cb8d21882874928c4cf30809f16e73da Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:35:15 +0400 Subject: [PATCH 008/123] feat: Add AddProvider extension method in OpenFeatureServiceCollectionExtensions --- OpenFeature.sln | 9 ++++- .../OpenFeature.DependencyInjection.csproj | 2 +- ...enFeature.DependencyInjection.Tests.csproj | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj diff --git a/OpenFeature.sln b/OpenFeature.sln index 7fccd63f..f986b777 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -79,7 +79,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -107,6 +109,10 @@ Global {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +129,7 @@ Global {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index f6af26ef..56b00cb7 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - net6.0; + net6.0;net8.0 enable enable OpenFeature diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj new file mode 100644 index 00000000..9937e1bc --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0;net8.0 + enable + enable + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 5905cc666f4be5157f0672b50a208ff24563749f Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:35:47 +0400 Subject: [PATCH 009/123] test: Add OpenFeatureBuilderExtensionsTests --- Directory.Packages.props | 1 + .../OpenFeatureBuilderExtensionsTests.cs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ed54c052..617bec39 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 00000000..f66cfef4 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,55 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class OpenFeatureBuilderExtensionsTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = useServiceProviderDelegate ? + builder.AddContext(_ => { }) : + builder.AddContext((_, _) => { }); + + // Assert + result.Should().BeSameAs(builder, "The method should return the same builder instance."); + services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(EvaluationContext) && + serviceDescriptor.Lifetime == ServiceLifetime.Transient, + "A transient service of type EvaluationContext should be added."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + bool delegateCalled = false; + + _ = useServiceProviderDelegate ? + builder.AddContext(_ => delegateCalled = true) : + builder.AddContext((_, _) => delegateCalled = true); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var context = serviceProvider.GetService(); + + // Assert + context.Should().NotBeNull("The EvaluationContext should be resolvable."); + delegateCalled.Should().BeTrue("The delegate should be invoked."); + } +} From 6c3c15e77ae9ae55be2bd4df2d711b45f2aa823c Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:47:04 +0400 Subject: [PATCH 010/123] test: Add unit tests for AddProvider method in OpenFeatureBuilderExtensionsTests --- .../Mocks/NotImplementedFeatureProvider.cs | 39 +++++++++++++++++++ .../OpenFeatureBuilderExtensionsTests.cs | 38 +++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs new file mode 100644 index 00000000..a7a857dd --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs @@ -0,0 +1,39 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +public partial class OpenFeatureBuilderExtensionsTests +{ + public class NotImplementedFeatureProvider : FeatureProvider + { + public override Metadata? GetMetadata() + { + throw new NotImplementedException(); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index f66cfef4..67099f3f 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -5,7 +5,7 @@ namespace OpenFeature.DependencyInjection.Tests; -public class OpenFeatureBuilderExtensionsTests +public partial class OpenFeatureBuilderExtensionsTests { [Theory] [InlineData(true)] @@ -52,4 +52,40 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe context.Should().NotBeNull("The EvaluationContext should be resolvable."); delegateCalled.Should().BeTrue("The delegate should be invoked."); } + + [Fact] + public void AddProvider_ShouldAddProviderToCollection() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.AddProvider(_ => new NotImplementedFeatureProvider()); + + // Assert + result.Should().BeSameAs(builder, "The method should return the same builder instance."); + services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(FeatureProvider) && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + "A singleton service of type FeatureProvider should be added."); + } + + [Fact] + public void AddProvider_ShouldResolveCorrectProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + builder.AddProvider(_ => new NotImplementedFeatureProvider()); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetService(); + + // Assert + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } } From bd20d40c3f4557de5429e2556d2c76091f47e9e6 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:04:30 +0400 Subject: [PATCH 011/123] feat: Replicate NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider --- .../Mocks/NotImplementedFeatureProvider.cs | 39 -------------- .../NoOpFeatureProvider.cs | 52 +++++++++++++++++++ .../NoOpProvider.cs | 8 +++ 3 files changed, 60 insertions(+), 39 deletions(-) delete mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs deleted file mode 100644 index a7a857dd..00000000 --- a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using OpenFeature.Model; - -namespace OpenFeature.DependencyInjection.Tests; - -public partial class OpenFeatureBuilderExtensionsTests -{ - public class NotImplementedFeatureProvider : FeatureProvider - { - public override Metadata? GetMetadata() - { - throw new NotImplementedException(); - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } -} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs new file mode 100644 index 00000000..ac3e5209 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs @@ -0,0 +1,52 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs. +// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class. +// If the InternalsVisibleTo attribute is added to the OpenFeature project, +// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing. +internal sealed class NoOpFeatureProvider : FeatureProvider +{ + private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); + + public override Metadata GetMetadata() + { + return this._metadata; + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) + { + return new ResolutionDetails( + flagKey, + defaultValue, + reason: NoOpProvider.ReasonNoOp, + variant: NoOpProvider.Variant + ); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs new file mode 100644 index 00000000..7bf20bca --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs @@ -0,0 +1,8 @@ +namespace OpenFeature.DependencyInjection.Tests; + +internal static class NoOpProvider +{ + public const string NoOpProviderName = "No-op Provider"; + public const string ReasonNoOp = "No-op"; + public const string Variant = "No-op"; +} From 441aa4f5a640aedffa836cf6ab7360dc37883f95 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:05:09 +0400 Subject: [PATCH 012/123] test: Update OpenFeatureBuilderExtensionsTests with NoOpFeatureProvider usage --- .../OpenFeatureBuilderExtensionsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 67099f3f..da7742b6 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -61,7 +61,7 @@ public void AddProvider_ShouldAddProviderToCollection() var builder = new OpenFeatureBuilder(services); // Act - var result = builder.AddProvider(_ => new NotImplementedFeatureProvider()); + var result = builder.AddProvider(_ => new NoOpFeatureProvider()); // Assert result.Should().BeSameAs(builder, "The method should return the same builder instance."); @@ -77,7 +77,7 @@ public void AddProvider_ShouldResolveCorrectProvider() // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - builder.AddProvider(_ => new NotImplementedFeatureProvider()); + builder.AddProvider(_ => new NoOpFeatureProvider()); var serviceProvider = services.BuildServiceProvider(); @@ -86,6 +86,6 @@ public void AddProvider_ShouldResolveCorrectProvider() // Assert provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } } From fa046656e8dc926c95c1e3b44a9fbdabe3250902 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:05:36 +0400 Subject: [PATCH 013/123] test: Add OpenFeatureBuilderExtensionsTests --- .../OpenFeature.DependencyInjection.csproj | 9 +++ .../FeatureLifecycleManagerTests.cs | 57 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 56b00cb7..775d4d6d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -15,4 +15,13 @@ + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs new file mode 100644 index 00000000..8b9823e0 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using OpenFeature.Internal; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class FeatureLifecycleManagerTests +{ + private readonly FeatureLifecycleManager _systemUnderTest; + private readonly IServiceProvider _mockServiceProvider; + + public FeatureLifecycleManagerTests() + { + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + + //_mockApi = Substitute.ForPartsOf(); + //Api.Instance.Returns(_mockApi); + + _mockServiceProvider = Substitute.For(); + + _systemUnderTest = new FeatureLifecycleManager( + Api.Instance, + _mockServiceProvider, + Substitute.For>()); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists() + { + // Arrange + var featureProvider = new NoOpFeatureProvider(); + _mockServiceProvider.GetService(typeof(FeatureProvider)) + .Returns(featureProvider); + + // Act + await _systemUnderTest.EnsureInitializedAsync(); + + // Assert + Api.Instance.GetProvider().Should().BeSameAs(featureProvider); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist() + { + // Arrange + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(false)); + + exception.Message.Should().Be("Feature provider is not registered in the service collection."); + } +} From fcea8a6c954332fe31bdf2662397975b44426753 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:06:42 +0400 Subject: [PATCH 014/123] refactor: Remove ExcludeFromCodeCoverage attribute from FeatureLifecycleManager --- .../Internal/FeatureLifecycleManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 1b2f47c9..4b6ad426 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature.Internal; -[ExcludeFromCodeCoverage] internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager { private readonly Api _featureApi; From fc8dcab2104e9484c58b903b9229bd26220938e4 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:10:58 +0400 Subject: [PATCH 015/123] test: Improve OpenFeatureBuilderExtensionsTests for better coverage and clarity --- .../OpenFeatureBuilderExtensionsTests.cs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index da7742b6..d4726db5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -7,23 +7,29 @@ namespace OpenFeature.DependencyInjection.Tests; public partial class OpenFeatureBuilderExtensionsTests { + private readonly IServiceCollection _services; + private readonly OpenFeatureBuilder _systemUnderTest; + + public OpenFeatureBuilderExtensionsTests() + { + _services = new ServiceCollection(); + _systemUnderTest = new OpenFeatureBuilder(_services); + } + [Theory] [InlineData(true)] [InlineData(false)] public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - // Act var result = useServiceProviderDelegate ? - builder.AddContext(_ => { }) : - builder.AddContext((_, _) => { }); + _systemUnderTest.AddContext(_ => { }) : + _systemUnderTest.AddContext((_, _) => { }); // Assert - result.Should().BeSameAs(builder, "The method should return the same builder instance."); - services.Should().ContainSingle(serviceDescriptor => + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(EvaluationContext) && serviceDescriptor.Lifetime == ServiceLifetime.Transient, "A transient service of type EvaluationContext should be added."); @@ -35,20 +41,19 @@ public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProv public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) { // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); bool delegateCalled = false; _ = useServiceProviderDelegate ? - builder.AddContext(_ => delegateCalled = true) : - builder.AddContext((_, _) => delegateCalled = true); + _systemUnderTest.AddContext(_ => delegateCalled = true) : + _systemUnderTest.AddContext((_, _) => delegateCalled = true); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = _services.BuildServiceProvider(); // Act var context = serviceProvider.GetService(); // Assert + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); context.Should().NotBeNull("The EvaluationContext should be resolvable."); delegateCalled.Should().BeTrue("The delegate should be invoked."); } @@ -56,16 +61,13 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe [Fact] public void AddProvider_ShouldAddProviderToCollection() { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - // Act - var result = builder.AddProvider(_ => new NoOpFeatureProvider()); + var result = _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); // Assert - result.Should().BeSameAs(builder, "The method should return the same builder instance."); - services.Should().ContainSingle(serviceDescriptor => + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(FeatureProvider) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton, "A singleton service of type FeatureProvider should be added."); @@ -75,16 +77,15 @@ public void AddProvider_ShouldAddProviderToCollection() public void AddProvider_ShouldResolveCorrectProvider() { // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - builder.AddProvider(_ => new NoOpFeatureProvider()); + _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = _services.BuildServiceProvider(); // Act var provider = serviceProvider.GetService(); // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); provider.Should().NotBeNull("The FeatureProvider should be resolvable."); provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } From 0a996aaed24da05303177822e2ae57cc90e0da98 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:11:53 +0400 Subject: [PATCH 016/123] test: Create OpenFeatureServiceCollectionExtensionsTests --- .../OpenFeatureServiceCollectionExtensions.cs | 2 - ...FeatureServiceCollectionExtensionsTests.cs | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 17ffc7e8..88061f55 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -2,14 +2,12 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using OpenFeature.Internal; using OpenFeature.Model; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature; /// /// Contains extension methods for the class. /// -[ExcludeFromCodeCoverage] public static class OpenFeatureServiceCollectionExtensions { /// diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..4149b8a8 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace OpenFeature.Tests; + +public class OpenFeatureServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _systemUnderTest; + private readonly Action _configureAction; + + public OpenFeatureServiceCollectionExtensionsTests() + { + _systemUnderTest = new ServiceCollection(); + _configureAction = Substitute.For>(); + } + + [Fact] + public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSingleton() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + _systemUnderTest.Should().HaveCount(3); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); + } + + [Fact] + public void AddOpenFeature_ShouldInvokeConfigureAction() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + // Assert + _configureAction.Received(1).Invoke(Arg.Any()); + } +} From 76e1816f1eacd6c6facb752f8b9cb3d3bf3f7516 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:35:49 +0400 Subject: [PATCH 017/123] feat: Create OpenFeature.Hosting project --- Directory.Packages.props | 11 ++++++----- OpenFeature.sln | 9 ++++++++- src/OpenFeature.Hosting/OpenFeature.Hosting.csproj | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 src/OpenFeature.Hosting/OpenFeature.Hosting.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 617bec39..8e4347e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,18 +1,19 @@ - + true - + + - + @@ -29,9 +30,9 @@ - + - + diff --git a/OpenFeature.sln b/OpenFeature.sln index f986b777..5ce227c5 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -81,7 +81,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -113,6 +115,10 @@ Global {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +136,7 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj new file mode 100644 index 00000000..30771b45 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -0,0 +1,14 @@ + + + + net6.0;net8.0 + enable + enable + OpenFeature + + + + + + + From 84dd0e0462fa92c64044fe8c38427a27e998ce8f Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 22:50:49 +0400 Subject: [PATCH 018/123] feat: Add HostedFeatureLifecycleService and related logic --- .../FeatureLifecycleStateOptions.cs | 18 ++++ src/OpenFeature.Hosting/FeatureStartState.cs | 22 +++++ src/OpenFeature.Hosting/FeatureStopState.cs | 22 +++++ .../HostedFeatureLifecycleService.cs | 93 +++++++++++++++++++ .../OpenFeature.Hosting.csproj | 4 + .../OpenFeatureBuilderExtensions.cs | 35 +++++++ 6 files changed, 194 insertions(+) create mode 100644 src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs create mode 100644 src/OpenFeature.Hosting/FeatureStartState.cs create mode 100644 src/OpenFeature.Hosting/FeatureStopState.cs create mode 100644 src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs create mode 100644 src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs new file mode 100644 index 00000000..91e3047d --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs @@ -0,0 +1,18 @@ +namespace OpenFeature; + +/// +/// Represents the lifecycle state options for a feature, +/// defining the states during the start and stop lifecycle. +/// +public class FeatureLifecycleStateOptions +{ + /// + /// Gets or sets the state during the feature startup lifecycle. + /// + public FeatureStartState StartState { get; set; } = FeatureStartState.Starting; + + /// + /// Gets or sets the state during the feature shutdown lifecycle. + /// + public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping; +} diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs new file mode 100644 index 00000000..3aa59179 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStartState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for starting a feature. +/// +public enum FeatureStartState +{ + /// + /// The feature is in the process of starting. + /// + Starting, + + /// + /// The feature is at the start state. + /// + Start, + + /// + /// The feature has fully started. + /// + Started +} diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs new file mode 100644 index 00000000..a8298da0 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStopState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for stopping a feature. +/// +public enum FeatureStopState +{ + /// + /// The feature is in the process of stopping. + /// + Stopping, + + /// + /// The feature is at the stop state. + /// + Stop, + + /// + /// The feature has fully stopped. + /// + Stopped +} diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs new file mode 100644 index 00000000..1e3b3c30 --- /dev/null +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenFeature; + +/// +/// A hosted service that manages the lifecycle of features within the application. +/// It ensures that features are properly initialized when the service starts +/// and gracefully shuts down when the service stops. +/// +public sealed class HostedFeatureLifecycleService : IHostedLifecycleService +{ + private readonly ILogger _logger; + private readonly IFeatureLifecycleManager _featureLifecycleManager; + private readonly IOptions _featureLifecycleStateOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The logger used to log lifecycle events. + /// The feature lifecycle manager responsible for initialization and shutdown. + /// Options that define the start and stop states of the feature lifecycle. + public HostedFeatureLifecycleService( + ILogger logger, + IFeatureLifecycleManager featureLifecycleManager, + IOptions featureLifecycleStateOptions) + { + _logger = logger; + _featureLifecycleManager = featureLifecycleManager; + _featureLifecycleStateOptions = featureLifecycleStateOptions; + } + + /// + /// Ensures that the feature is properly initialized when the service starts. + /// + public async Task StartingAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Starting, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Start" state. + /// + public async Task StartAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Start, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully started and operational. + /// + public async Task StartedAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Started, cancellationToken).ConfigureAwait(false); + + /// + /// Gracefully shuts down the feature when the service is stopping. + /// + public async Task StoppingAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopping, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Stop" state. + /// + public async Task StopAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stop, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully stopped and no longer operational. + /// + public async Task StoppedAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopped, cancellationToken).ConfigureAwait(false); + + /// + /// Initializes the feature lifecycle if the current state matches the expected start state. + /// + private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StartState == expectedState) + { + _logger.LogInformation("Initializing the Feature Lifecycle Manager for state {State}.", expectedState); + await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Shuts down the feature lifecycle if the current state matches the expected stop state. + /// + private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StopState == expectedState) + { + _logger.LogInformation("Shutting down the Feature Lifecycle Manager for state {State}.", expectedState); + await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj index 30771b45..48730084 100644 --- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..d45efd1c --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature; + +/// +/// Extension methods for configuring the hosted feature lifecycle in the . +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// Adds the to the OpenFeatureBuilder, + /// which manages the lifecycle of features within the application. It also allows + /// configuration of the . + /// + /// The instance. + /// An optional action to configure . + /// The instance. + public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) + { + if(configureOptions == null) + { + builder.Services.Configure(cfg => { + cfg.StartState = FeatureStartState.Starting; + cfg.StopState = FeatureStopState.Stopping; + }); + } + else + { + builder.Services.Configure(configureOptions); + } + + builder.Services.AddHostedService(); + return builder; + } +} From 572d67bca53c69b2cd353001e66f95a1f5f7f58f Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 14 Oct 2024 08:53:43 +0400 Subject: [PATCH 019/123] test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist test --- .../FeatureLifecycleManagerTests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 8b9823e0..903be3cf 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -16,9 +16,6 @@ public FeatureLifecycleManagerTests() Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - //_mockApi = Substitute.ForPartsOf(); - //Api.Instance.Returns(_mockApi); - _mockServiceProvider = Substitute.For(); _systemUnderTest = new FeatureLifecycleManager( @@ -48,10 +45,11 @@ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNo // Arrange _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(false)); + // Act + var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); + // Assert + var exception = await Assert.ThrowsAsync(act); exception.Message.Should().Be("Feature provider is not registered in the service collection."); } } From d7e5fbbf4808191f0cbe43fd5eb8fa82bd70dc21 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:31:38 +0400 Subject: [PATCH 020/123] [SIP-123] feat: Create OpenFeature.DependencyInjection project Signed-off-by: Artyom Tonoyan --- OpenFeature.sln | 21 ++++++++++++------- .../OpenFeature.DependencyInjection.csproj | 9 ++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj diff --git a/OpenFeature.sln b/OpenFeature.sln index 6f1cce8d..7fccd63f 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -77,7 +77,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -101,21 +103,26 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj new file mode 100644 index 00000000..edd721a3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -0,0 +1,9 @@ + + + + netstandard2.0;net6.0;net8.0;net462 + enable + enable + + + From f60b5d83faf6a3580354e8c0be9038ee20c7aec4 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:37:43 +0400 Subject: [PATCH 021/123] chore: Update TargetFrameworks to net6.0 Signed-off-by: Artyom Tonoyan --- .../OpenFeature.DependencyInjection.csproj | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index edd721a3..4589884a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,9 +1,14 @@ - netstandard2.0;net6.0;net8.0;net462 + net6.0; enable enable + OpenFeature + + + + From 8b59e53a386798a08d6ee917968cd2fbe0708cb8 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:38:33 +0400 Subject: [PATCH 022/123] feat: Create OpenFeatureBuilder record Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilder.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs new file mode 100644 index 00000000..03cad3ca --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature; + +/// +/// Describes a backed by an . +/// +/// +public sealed record OpenFeatureBuilder(IServiceCollection Services) +{ + /// + /// Indicates whether the evaluation context has been configured. + /// This property is used to determine if specific configurations or services + /// should be initialized based on the presence of an evaluation context. + /// + internal bool IsContextConfigured { get; set; } +} From c644df61e12bf6b8bf5fa810547c5fe8cec382a9 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:41:45 +0400 Subject: [PATCH 023/123] feat: Add OpenFeatureBuilderExtensions Signed-off-by: Artyom Tonoyan --- .../OpenFeature.DependencyInjection.csproj | 4 ++ .../OpenFeatureBuilderExtensions.cs | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 4589884a..f6af26ef 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..54e20426 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.Model; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// This method is used to add a new context to the service collection. + /// + /// + /// the desired configuration + /// + /// the instance + /// + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + return builder.AddContext((b, _) => configure(b)); + } + + /// + /// This method is used to add a new context to the service collection. + /// + /// + /// the desired configuration + /// + /// the instance + /// + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + builder.IsContextConfigured = true; + builder.Services.TryAddTransient(provider => { + var contextBuilder = EvaluationContext.Builder(); + configure(contextBuilder, provider); + return contextBuilder.Build(); + }); + + return builder; + } +} From f4f108b4b6aee6b01c1e5d886b1b01f1ebf306f0 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:48:35 +0400 Subject: [PATCH 024/123] feat: Add IFeatureLifecycleManager interface and implementation Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 1 + .../IFeatureLifecycleManager.cs | 24 ++++++++++++ .../Internal/FeatureLifecycleManager.cs | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7227000a..ed54c052 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs new file mode 100644 index 00000000..2085bda4 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs @@ -0,0 +1,24 @@ +namespace OpenFeature; + +/// +/// Defines the contract for managing the lifecycle of a feature api. +/// +public interface IFeatureLifecycleManager +{ + /// + /// Ensures that the feature provider is properly initialized and ready to be used. + /// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of initializing the feature provider. + /// Thrown when the feature provider is not registered or is in an invalid state. + ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default); + + /// + /// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved. + /// This method should handle all necessary cleanup and shutdown operations for the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of shutting down the feature provider. + ValueTask ShutdownAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs new file mode 100644 index 00000000..1b2f47c9 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature.Internal; + +[ExcludeFromCodeCoverage] +internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager +{ + private readonly Api _featureApi; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger logger) + { + _featureApi = featureApi; + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting initialization of the feature provider"); + var featureProvider = _serviceProvider.GetService(); + if (featureProvider == null) + { + throw new InvalidOperationException("Feature provider is not registered in the service collection."); + } + await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); + } + + /// + public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Shutting down the feature provider."); + await _featureApi.ShutdownAsync().ConfigureAwait(false); + } +} From 43bcde975b1b7bc6f7352a73f62807194e6ef711 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:51:20 +0400 Subject: [PATCH 025/123] feat: Add OpenFeatureServiceCollectionExtensions Signed-off-by: Artyom Tonoyan --- .../OpenFeatureServiceCollectionExtensions.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs new file mode 100644 index 00000000..17ffc7e8 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.Internal; +using OpenFeature.Model; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +[ExcludeFromCodeCoverage] +public static class OpenFeatureServiceCollectionExtensions +{ + /// + /// This method is used to add OpenFeature to the service collection. + /// OpenFeature will be registered as a singleton. + /// + /// + /// the desired configuration + /// the current instance + public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + services.TryAddSingleton(Api.Instance); + services.TryAddSingleton(); + + var builder = new OpenFeatureBuilder(services); + configure(builder); + + if (builder.IsContextConfigured) + { + services.TryAddScoped(static provider => { + var api = provider.GetRequiredService(); + var client = api.GetClient(); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + services.TryAddScoped(static provider => { + var api = provider.GetRequiredService(); + return api.GetClient(); + }); + } + + return services; + } +} From 47e87c25436fb5e7095beb9036c50d33cd7ce808 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 8 Oct 2024 23:16:26 +0400 Subject: [PATCH 026/123] feat: Add AddProvider extension method in OpenFeatureBuilderExtensions Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 54e20426..579b61ca 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -11,11 +11,9 @@ public static partial class OpenFeatureBuilderExtensions /// /// This method is used to add a new context to the service collection. /// - /// + /// The instance. /// the desired configuration - /// - /// the instance - /// + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -29,11 +27,9 @@ public static OpenFeatureBuilder AddContext( /// /// This method is used to add a new context to the service collection. /// - /// + /// The instance. /// the desired configuration - /// - /// the instance - /// + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -42,7 +38,8 @@ public static OpenFeatureBuilder AddContext( ArgumentNullException.ThrowIfNull(configure); builder.IsContextConfigured = true; - builder.Services.TryAddTransient(provider => { + builder.Services.TryAddTransient(provider => + { var contextBuilder = EvaluationContext.Builder(); configure(contextBuilder, provider); return contextBuilder.Build(); @@ -50,4 +47,19 @@ public static OpenFeatureBuilder AddContext( return builder; } + + /// + /// Adds a feature provider to the service collection. + /// + /// The type of the feature provider, which must inherit from . + /// The instance. + /// A factory method to create the feature provider, using the service provider. + /// The instance. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory) + where T : FeatureProvider + { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.TryAddSingleton(providerFactory); + return builder; + } } From 3650de27fac81d5966f47a3445a6d0cf29a20a29 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:35:15 +0400 Subject: [PATCH 027/123] feat: Add AddProvider extension method in OpenFeatureServiceCollectionExtensions Signed-off-by: Artyom Tonoyan --- OpenFeature.sln | 9 ++++- .../OpenFeature.DependencyInjection.csproj | 2 +- ...enFeature.DependencyInjection.Tests.csproj | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj diff --git a/OpenFeature.sln b/OpenFeature.sln index 7fccd63f..f986b777 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -79,7 +79,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -107,6 +109,10 @@ Global {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +129,7 @@ Global {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index f6af26ef..56b00cb7 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - net6.0; + net6.0;net8.0 enable enable OpenFeature diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj new file mode 100644 index 00000000..9937e1bc --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0;net8.0 + enable + enable + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 0027489a9f74b6fea7ad66e53a7d790979525123 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:35:47 +0400 Subject: [PATCH 028/123] test: Add OpenFeatureBuilderExtensionsTests Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 1 + .../OpenFeatureBuilderExtensionsTests.cs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ed54c052..617bec39 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 00000000..f66cfef4 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,55 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class OpenFeatureBuilderExtensionsTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = useServiceProviderDelegate ? + builder.AddContext(_ => { }) : + builder.AddContext((_, _) => { }); + + // Assert + result.Should().BeSameAs(builder, "The method should return the same builder instance."); + services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(EvaluationContext) && + serviceDescriptor.Lifetime == ServiceLifetime.Transient, + "A transient service of type EvaluationContext should be added."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + bool delegateCalled = false; + + _ = useServiceProviderDelegate ? + builder.AddContext(_ => delegateCalled = true) : + builder.AddContext((_, _) => delegateCalled = true); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var context = serviceProvider.GetService(); + + // Assert + context.Should().NotBeNull("The EvaluationContext should be resolvable."); + delegateCalled.Should().BeTrue("The delegate should be invoked."); + } +} From 563816392542a6f241e7b48d1bc29d6d8cc0dd9c Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:47:04 +0400 Subject: [PATCH 029/123] test: Add unit tests for AddProvider method in OpenFeatureBuilderExtensionsTests Signed-off-by: Artyom Tonoyan --- .../Mocks/NotImplementedFeatureProvider.cs | 39 +++++++++++++++++++ .../OpenFeatureBuilderExtensionsTests.cs | 38 +++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs new file mode 100644 index 00000000..a7a857dd --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs @@ -0,0 +1,39 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +public partial class OpenFeatureBuilderExtensionsTests +{ + public class NotImplementedFeatureProvider : FeatureProvider + { + public override Metadata? GetMetadata() + { + throw new NotImplementedException(); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index f66cfef4..67099f3f 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -5,7 +5,7 @@ namespace OpenFeature.DependencyInjection.Tests; -public class OpenFeatureBuilderExtensionsTests +public partial class OpenFeatureBuilderExtensionsTests { [Theory] [InlineData(true)] @@ -52,4 +52,40 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe context.Should().NotBeNull("The EvaluationContext should be resolvable."); delegateCalled.Should().BeTrue("The delegate should be invoked."); } + + [Fact] + public void AddProvider_ShouldAddProviderToCollection() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.AddProvider(_ => new NotImplementedFeatureProvider()); + + // Assert + result.Should().BeSameAs(builder, "The method should return the same builder instance."); + services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(FeatureProvider) && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + "A singleton service of type FeatureProvider should be added."); + } + + [Fact] + public void AddProvider_ShouldResolveCorrectProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + builder.AddProvider(_ => new NotImplementedFeatureProvider()); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetService(); + + // Assert + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } } From 5de7cebcbc0d22a18cb0fb8de51311af6a73dfbe Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:04:30 +0400 Subject: [PATCH 030/123] feat: Replicate NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider Signed-off-by: Artyom Tonoyan --- .../Mocks/NotImplementedFeatureProvider.cs | 39 -------------- .../NoOpFeatureProvider.cs | 52 +++++++++++++++++++ .../NoOpProvider.cs | 8 +++ 3 files changed, 60 insertions(+), 39 deletions(-) delete mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs deleted file mode 100644 index a7a857dd..00000000 --- a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using OpenFeature.Model; - -namespace OpenFeature.DependencyInjection.Tests; - -public partial class OpenFeatureBuilderExtensionsTests -{ - public class NotImplementedFeatureProvider : FeatureProvider - { - public override Metadata? GetMetadata() - { - throw new NotImplementedException(); - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } -} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs new file mode 100644 index 00000000..ac3e5209 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs @@ -0,0 +1,52 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs. +// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class. +// If the InternalsVisibleTo attribute is added to the OpenFeature project, +// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing. +internal sealed class NoOpFeatureProvider : FeatureProvider +{ + private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); + + public override Metadata GetMetadata() + { + return this._metadata; + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) + { + return new ResolutionDetails( + flagKey, + defaultValue, + reason: NoOpProvider.ReasonNoOp, + variant: NoOpProvider.Variant + ); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs new file mode 100644 index 00000000..7bf20bca --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs @@ -0,0 +1,8 @@ +namespace OpenFeature.DependencyInjection.Tests; + +internal static class NoOpProvider +{ + public const string NoOpProviderName = "No-op Provider"; + public const string ReasonNoOp = "No-op"; + public const string Variant = "No-op"; +} From 2e99210f2ece7c1f4e40007557e309533a89ce17 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:05:09 +0400 Subject: [PATCH 031/123] test: Update OpenFeatureBuilderExtensionsTests with NoOpFeatureProvider usage Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensionsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 67099f3f..da7742b6 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -61,7 +61,7 @@ public void AddProvider_ShouldAddProviderToCollection() var builder = new OpenFeatureBuilder(services); // Act - var result = builder.AddProvider(_ => new NotImplementedFeatureProvider()); + var result = builder.AddProvider(_ => new NoOpFeatureProvider()); // Assert result.Should().BeSameAs(builder, "The method should return the same builder instance."); @@ -77,7 +77,7 @@ public void AddProvider_ShouldResolveCorrectProvider() // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - builder.AddProvider(_ => new NotImplementedFeatureProvider()); + builder.AddProvider(_ => new NoOpFeatureProvider()); var serviceProvider = services.BuildServiceProvider(); @@ -86,6 +86,6 @@ public void AddProvider_ShouldResolveCorrectProvider() // Assert provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } } From 880df923fa95fb59f18af4e97815b7ba68fb8525 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:05:36 +0400 Subject: [PATCH 032/123] test: Add OpenFeatureBuilderExtensionsTests Signed-off-by: Artyom Tonoyan --- .../OpenFeature.DependencyInjection.csproj | 9 +++ .../FeatureLifecycleManagerTests.cs | 57 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 56b00cb7..775d4d6d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -15,4 +15,13 @@ + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs new file mode 100644 index 00000000..8b9823e0 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using OpenFeature.Internal; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class FeatureLifecycleManagerTests +{ + private readonly FeatureLifecycleManager _systemUnderTest; + private readonly IServiceProvider _mockServiceProvider; + + public FeatureLifecycleManagerTests() + { + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + + //_mockApi = Substitute.ForPartsOf(); + //Api.Instance.Returns(_mockApi); + + _mockServiceProvider = Substitute.For(); + + _systemUnderTest = new FeatureLifecycleManager( + Api.Instance, + _mockServiceProvider, + Substitute.For>()); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists() + { + // Arrange + var featureProvider = new NoOpFeatureProvider(); + _mockServiceProvider.GetService(typeof(FeatureProvider)) + .Returns(featureProvider); + + // Act + await _systemUnderTest.EnsureInitializedAsync(); + + // Assert + Api.Instance.GetProvider().Should().BeSameAs(featureProvider); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist() + { + // Arrange + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(false)); + + exception.Message.Should().Be("Feature provider is not registered in the service collection."); + } +} From 92587068415e95fd77d0af6f9c4b72699aeba7c9 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:06:42 +0400 Subject: [PATCH 033/123] refactor: Remove ExcludeFromCodeCoverage attribute from FeatureLifecycleManager Signed-off-by: Artyom Tonoyan --- .../Internal/FeatureLifecycleManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 1b2f47c9..4b6ad426 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature.Internal; -[ExcludeFromCodeCoverage] internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager { private readonly Api _featureApi; From 19664b007fdba37dcf9806abc9e591a8895060d9 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:10:58 +0400 Subject: [PATCH 034/123] test: Improve OpenFeatureBuilderExtensionsTests for better coverage and clarity Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensionsTests.cs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index da7742b6..d4726db5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -7,23 +7,29 @@ namespace OpenFeature.DependencyInjection.Tests; public partial class OpenFeatureBuilderExtensionsTests { + private readonly IServiceCollection _services; + private readonly OpenFeatureBuilder _systemUnderTest; + + public OpenFeatureBuilderExtensionsTests() + { + _services = new ServiceCollection(); + _systemUnderTest = new OpenFeatureBuilder(_services); + } + [Theory] [InlineData(true)] [InlineData(false)] public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - // Act var result = useServiceProviderDelegate ? - builder.AddContext(_ => { }) : - builder.AddContext((_, _) => { }); + _systemUnderTest.AddContext(_ => { }) : + _systemUnderTest.AddContext((_, _) => { }); // Assert - result.Should().BeSameAs(builder, "The method should return the same builder instance."); - services.Should().ContainSingle(serviceDescriptor => + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(EvaluationContext) && serviceDescriptor.Lifetime == ServiceLifetime.Transient, "A transient service of type EvaluationContext should be added."); @@ -35,20 +41,19 @@ public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProv public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) { // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); bool delegateCalled = false; _ = useServiceProviderDelegate ? - builder.AddContext(_ => delegateCalled = true) : - builder.AddContext((_, _) => delegateCalled = true); + _systemUnderTest.AddContext(_ => delegateCalled = true) : + _systemUnderTest.AddContext((_, _) => delegateCalled = true); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = _services.BuildServiceProvider(); // Act var context = serviceProvider.GetService(); // Assert + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); context.Should().NotBeNull("The EvaluationContext should be resolvable."); delegateCalled.Should().BeTrue("The delegate should be invoked."); } @@ -56,16 +61,13 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe [Fact] public void AddProvider_ShouldAddProviderToCollection() { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - // Act - var result = builder.AddProvider(_ => new NoOpFeatureProvider()); + var result = _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); // Assert - result.Should().BeSameAs(builder, "The method should return the same builder instance."); - services.Should().ContainSingle(serviceDescriptor => + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(FeatureProvider) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton, "A singleton service of type FeatureProvider should be added."); @@ -75,16 +77,15 @@ public void AddProvider_ShouldAddProviderToCollection() public void AddProvider_ShouldResolveCorrectProvider() { // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - builder.AddProvider(_ => new NoOpFeatureProvider()); + _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = _services.BuildServiceProvider(); // Act var provider = serviceProvider.GetService(); // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); provider.Should().NotBeNull("The FeatureProvider should be resolvable."); provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } From 9c21943c2b290d3cc0305b613fde1827218264e9 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:11:53 +0400 Subject: [PATCH 035/123] test: Create OpenFeatureServiceCollectionExtensionsTests Signed-off-by: Artyom Tonoyan --- .../OpenFeatureServiceCollectionExtensions.cs | 2 - ...FeatureServiceCollectionExtensionsTests.cs | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 17ffc7e8..88061f55 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -2,14 +2,12 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using OpenFeature.Internal; using OpenFeature.Model; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature; /// /// Contains extension methods for the class. /// -[ExcludeFromCodeCoverage] public static class OpenFeatureServiceCollectionExtensions { /// diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..4149b8a8 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace OpenFeature.Tests; + +public class OpenFeatureServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _systemUnderTest; + private readonly Action _configureAction; + + public OpenFeatureServiceCollectionExtensionsTests() + { + _systemUnderTest = new ServiceCollection(); + _configureAction = Substitute.For>(); + } + + [Fact] + public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSingleton() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + _systemUnderTest.Should().HaveCount(3); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); + } + + [Fact] + public void AddOpenFeature_ShouldInvokeConfigureAction() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + // Assert + _configureAction.Received(1).Invoke(Arg.Any()); + } +} From a23c5ea96aca31ade34d4d542c349a997d127b04 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:35:49 +0400 Subject: [PATCH 036/123] feat: Create OpenFeature.Hosting project Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 11 ++++++----- OpenFeature.sln | 9 ++++++++- src/OpenFeature.Hosting/OpenFeature.Hosting.csproj | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 src/OpenFeature.Hosting/OpenFeature.Hosting.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 617bec39..8e4347e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,18 +1,19 @@ - + true - + + - + @@ -29,9 +30,9 @@ - + - + diff --git a/OpenFeature.sln b/OpenFeature.sln index f986b777..5ce227c5 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -81,7 +81,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -113,6 +115,10 @@ Global {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +136,7 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj new file mode 100644 index 00000000..30771b45 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -0,0 +1,14 @@ + + + + net6.0;net8.0 + enable + enable + OpenFeature + + + + + + + From 7906a3b39ab14eccc615dbe9c650992178ff6a72 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 22:50:49 +0400 Subject: [PATCH 037/123] feat: Add HostedFeatureLifecycleService and related logic Signed-off-by: Artyom Tonoyan --- .../FeatureLifecycleStateOptions.cs | 18 ++++ src/OpenFeature.Hosting/FeatureStartState.cs | 22 +++++ src/OpenFeature.Hosting/FeatureStopState.cs | 22 +++++ .../HostedFeatureLifecycleService.cs | 93 +++++++++++++++++++ .../OpenFeature.Hosting.csproj | 4 + .../OpenFeatureBuilderExtensions.cs | 35 +++++++ 6 files changed, 194 insertions(+) create mode 100644 src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs create mode 100644 src/OpenFeature.Hosting/FeatureStartState.cs create mode 100644 src/OpenFeature.Hosting/FeatureStopState.cs create mode 100644 src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs create mode 100644 src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs new file mode 100644 index 00000000..91e3047d --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs @@ -0,0 +1,18 @@ +namespace OpenFeature; + +/// +/// Represents the lifecycle state options for a feature, +/// defining the states during the start and stop lifecycle. +/// +public class FeatureLifecycleStateOptions +{ + /// + /// Gets or sets the state during the feature startup lifecycle. + /// + public FeatureStartState StartState { get; set; } = FeatureStartState.Starting; + + /// + /// Gets or sets the state during the feature shutdown lifecycle. + /// + public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping; +} diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs new file mode 100644 index 00000000..3aa59179 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStartState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for starting a feature. +/// +public enum FeatureStartState +{ + /// + /// The feature is in the process of starting. + /// + Starting, + + /// + /// The feature is at the start state. + /// + Start, + + /// + /// The feature has fully started. + /// + Started +} diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs new file mode 100644 index 00000000..a8298da0 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStopState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for stopping a feature. +/// +public enum FeatureStopState +{ + /// + /// The feature is in the process of stopping. + /// + Stopping, + + /// + /// The feature is at the stop state. + /// + Stop, + + /// + /// The feature has fully stopped. + /// + Stopped +} diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs new file mode 100644 index 00000000..1e3b3c30 --- /dev/null +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenFeature; + +/// +/// A hosted service that manages the lifecycle of features within the application. +/// It ensures that features are properly initialized when the service starts +/// and gracefully shuts down when the service stops. +/// +public sealed class HostedFeatureLifecycleService : IHostedLifecycleService +{ + private readonly ILogger _logger; + private readonly IFeatureLifecycleManager _featureLifecycleManager; + private readonly IOptions _featureLifecycleStateOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The logger used to log lifecycle events. + /// The feature lifecycle manager responsible for initialization and shutdown. + /// Options that define the start and stop states of the feature lifecycle. + public HostedFeatureLifecycleService( + ILogger logger, + IFeatureLifecycleManager featureLifecycleManager, + IOptions featureLifecycleStateOptions) + { + _logger = logger; + _featureLifecycleManager = featureLifecycleManager; + _featureLifecycleStateOptions = featureLifecycleStateOptions; + } + + /// + /// Ensures that the feature is properly initialized when the service starts. + /// + public async Task StartingAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Starting, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Start" state. + /// + public async Task StartAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Start, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully started and operational. + /// + public async Task StartedAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Started, cancellationToken).ConfigureAwait(false); + + /// + /// Gracefully shuts down the feature when the service is stopping. + /// + public async Task StoppingAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopping, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Stop" state. + /// + public async Task StopAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stop, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully stopped and no longer operational. + /// + public async Task StoppedAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopped, cancellationToken).ConfigureAwait(false); + + /// + /// Initializes the feature lifecycle if the current state matches the expected start state. + /// + private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StartState == expectedState) + { + _logger.LogInformation("Initializing the Feature Lifecycle Manager for state {State}.", expectedState); + await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Shuts down the feature lifecycle if the current state matches the expected stop state. + /// + private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StopState == expectedState) + { + _logger.LogInformation("Shutting down the Feature Lifecycle Manager for state {State}.", expectedState); + await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj index 30771b45..48730084 100644 --- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..d45efd1c --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature; + +/// +/// Extension methods for configuring the hosted feature lifecycle in the . +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// Adds the to the OpenFeatureBuilder, + /// which manages the lifecycle of features within the application. It also allows + /// configuration of the . + /// + /// The instance. + /// An optional action to configure . + /// The instance. + public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) + { + if(configureOptions == null) + { + builder.Services.Configure(cfg => { + cfg.StartState = FeatureStartState.Starting; + cfg.StopState = FeatureStopState.Stopping; + }); + } + else + { + builder.Services.Configure(configureOptions); + } + + builder.Services.AddHostedService(); + return builder; + } +} From 1850159864bd941edc646df4495c37ac4487e54a Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 14 Oct 2024 08:53:43 +0400 Subject: [PATCH 038/123] test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist test Signed-off-by: Artyom Tonoyan --- .../FeatureLifecycleManagerTests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 8b9823e0..903be3cf 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -16,9 +16,6 @@ public FeatureLifecycleManagerTests() Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - //_mockApi = Substitute.ForPartsOf(); - //Api.Instance.Returns(_mockApi); - _mockServiceProvider = Substitute.For(); _systemUnderTest = new FeatureLifecycleManager( @@ -48,10 +45,11 @@ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNo // Arrange _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(false)); + // Act + var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); + // Assert + var exception = await Assert.ThrowsAsync(act); exception.Message.Should().Be("Feature provider is not registered in the service collection."); } } From 70708ece71a050b51ee2c156232ca39cdb0392fd Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 15 Oct 2024 00:14:13 +0400 Subject: [PATCH 039/123] fix(build): Resolve MultiTarget framework build errors --- OpenFeature.sln | 2 +- .../CallerArgumentExpressionAttribute.cs | 23 +++++++++++++++++++ .../MultiTarget/Guard.cs | 20 ++++++++++++++++ .../MultiTarget/IsExternalInit.cs | 21 +++++++++++++++++ .../OpenFeature.DependencyInjection.csproj | 8 +++++-- .../OpenFeatureBuilder.cs | 2 +- .../OpenFeatureBuilderExtensions.cs | 10 ++++---- .../OpenFeatureServiceCollectionExtensions.cs | 4 ++-- 8 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs diff --git a/OpenFeature.sln b/OpenFeature.sln index 5ce227c5..e8191acd 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -83,7 +83,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjec EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..afbec6b0 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,23 @@ +// @formatter:off +// ReSharper disable All +#if NETCOREAPP3_0_OR_GREATER +// https://github.com/dotnet/runtime/issues/96197 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))] +#else +#pragma warning disable +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } +} +#endif diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs new file mode 100644 index 00000000..e086fc23 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace OpenFeature; + +[DebuggerStepThrough] +internal static class Guard +{ + public static T ThrowIfNull(T? value, [CallerArgumentExpression("value")] string name = null!) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value, name); +#else + if (value is null) + throw new ArgumentNullException(name); +#endif + + return value; + } +} diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs new file mode 100644 index 00000000..87714111 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs @@ -0,0 +1,21 @@ +// @formatter:off +// ReSharper disable All +#if NET5_0_OR_GREATER +// https://github.com/dotnet/runtime/issues/96197 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] +#else +#pragma warning disable +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +static class IsExternalInit { } +#endif diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 775d4d6d..fcdfd6ad 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - + - net6.0;net8.0 + netstandard2.0;net6.0;net8.0;net462 enable enable OpenFeature @@ -24,4 +24,8 @@ + + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index 03cad3ca..3ca3c10d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -5,7 +5,7 @@ namespace OpenFeature; /// /// Describes a backed by an . /// -/// +/// The instance. public sealed record OpenFeatureBuilder(IServiceCollection Services) { /// diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 579b61ca..8cc8e07a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -18,8 +18,8 @@ public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configure); + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); return builder.AddContext((b, _) => configure(b)); } @@ -34,8 +34,8 @@ public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configure); + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); builder.IsContextConfigured = true; builder.Services.TryAddTransient(provider => @@ -58,7 +58,7 @@ public static OpenFeatureBuilder AddContext( public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory) where T : FeatureProvider { - ArgumentNullException.ThrowIfNull(builder); + Guard.ThrowIfNull(builder); builder.Services.TryAddSingleton(providerFactory); return builder; } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 88061f55..863527af 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -19,8 +19,8 @@ public static class OpenFeatureServiceCollectionExtensions /// the current instance public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configure); + Guard.ThrowIfNull(services); + Guard.ThrowIfNull(configure); services.TryAddSingleton(Api.Instance); services.TryAddSingleton(); From d615b04c187539288ec968e871b13fe10c5bbd19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:20:51 +1000 Subject: [PATCH 040/123] chore(deps): update codecov/codecov-action action to v4.5.0 (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [codecov/codecov-action](https://togithub.com/codecov/codecov-action) | action | minor | `v4.3.1` -> `v4.5.0` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v4.5.0`](https://togithub.com/codecov/codecov-action/compare/v4.4.1...v4.5.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.4.1...v4.5.0) ### [`v4.4.1`](https://togithub.com/codecov/codecov-action/releases/tag/v4.4.1) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.4.0...v4.4.1) #### What's Changed - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 7.8.0 to 7.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1427](https://togithub.com/codecov/codecov-action/pull/1427) - fix: prevent xlarge from running on forks by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1432](https://togithub.com/codecov/codecov-action/pull/1432) - build(deps): bump github/codeql-action from 3.25.4 to 3.25.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1439](https://togithub.com/codecov/codecov-action/pull/1439) - build(deps): bump actions/checkout from 4.1.5 to 4.1.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1438](https://togithub.com/codecov/codecov-action/pull/1438) - fix: isPullRequestFromFork returns false for any PR by [@​shahar-h](https://togithub.com/shahar-h) in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) - chore(release): 4.4.1 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1441](https://togithub.com/codecov/codecov-action/pull/1441) #### New Contributors - [@​shahar-h](https://togithub.com/shahar-h) made their first contribution in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.4.0...v4.4.1 #### What's Changed - build(deps-dev): bump [@​typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) from 7.8.0 to 7.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1427](https://togithub.com/codecov/codecov-action/pull/1427) - fix: prevent xlarge from running on forks by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1432](https://togithub.com/codecov/codecov-action/pull/1432) - build(deps): bump github/codeql-action from 3.25.4 to 3.25.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1439](https://togithub.com/codecov/codecov-action/pull/1439) - build(deps): bump actions/checkout from 4.1.5 to 4.1.6 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1438](https://togithub.com/codecov/codecov-action/pull/1438) - fix: isPullRequestFromFork returns false for any PR by [@​shahar-h](https://togithub.com/shahar-h) in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) - chore(release): 4.4.1 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1441](https://togithub.com/codecov/codecov-action/pull/1441) #### New Contributors - [@​shahar-h](https://togithub.com/shahar-h) made their first contribution in [https://github.com/codecov/codecov-action/pull/1437](https://togithub.com/codecov/codecov-action/pull/1437) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.4.0...v4.4.1 ### [`v4.4.0`](https://togithub.com/codecov/codecov-action/releases/tag/v4.4.0) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v4.3.1...v4.4.0) #### What's Changed - chore: Clarify isPullRequestFromFork by [@​jsoref](https://togithub.com/jsoref) in [https://github.com/codecov/codecov-action/pull/1411](https://togithub.com/codecov/codecov-action/pull/1411) - build(deps): bump actions/checkout from 4.1.4 to 4.1.5 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1423](https://togithub.com/codecov/codecov-action/pull/1423) - build(deps): bump github/codeql-action from 3.25.3 to 3.25.4 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1421](https://togithub.com/codecov/codecov-action/pull/1421) - build(deps): bump ossf/scorecard-action from 2.3.1 to 2.3.3 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1420](https://togithub.com/codecov/codecov-action/pull/1420) - feat: remove GPG and run on spawn by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1426](https://togithub.com/codecov/codecov-action/pull/1426) - build(deps-dev): bump [@​typescript-eslint/parser](https://togithub.com/typescript-eslint/parser) from 7.8.0 to 7.9.0 by [@​dependabot](https://togithub.com/dependabot) in [https://github.com/codecov/codecov-action/pull/1428](https://togithub.com/codecov/codecov-action/pull/1428) - chore(release): 4.4.0 by [@​thomasrockhu-codecov](https://togithub.com/thomasrockhu-codecov) in [https://github.com/codecov/codecov-action/pull/1430](https://togithub.com/codecov/codecov-action/pull/1430) **Full Changelog**: https://github.com/codecov/codecov-action/compare/v4.3.1...v4.4.0
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- .github/workflows/code-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 010ed660..1f07ffc6 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -37,7 +37,7 @@ jobs: - name: Run Test run: dotnet test --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover - - uses: codecov/codecov-action@v4.3.1 + - uses: codecov/codecov-action@v4.5.0 with: name: Code Coverage for ${{ matrix.os }} fail_ci_if_error: true From ad9576522a65c1c28280220a9576687d47a8988b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:25:40 +1000 Subject: [PATCH 041/123] chore(deps): update dependency githubactionstestlogger to v2.4.1 (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [GitHubActionsTestLogger](https://togithub.com/Tyrrrz/GitHubActionsTestLogger) | `2.3.3` -> `2.4.1` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/GitHubActionsTestLogger/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/GitHubActionsTestLogger/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/GitHubActionsTestLogger/2.3.3/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/GitHubActionsTestLogger/2.3.3/2.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
Tyrrrz/GitHubActionsTestLogger (GitHubActionsTestLogger) ### [`v2.4.1`](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/releases/tag/2.4.1) [Compare Source](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/compare/2.4...2.4.1) #### What's Changed - Fix incorrect fallback for the "include not found tests" option by [@​Tyrrrz](https://togithub.com/Tyrrrz) in [https://github.com/Tyrrrz/GitHubActionsTestLogger/pull/27](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/pull/27) **Full Changelog**: https://github.com/Tyrrrz/GitHubActionsTestLogger/compare/2.4...2.4.1 ### [`v2.4.0`](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/compare/2.3.3...2.4) [Compare Source](https://togithub.com/Tyrrrz/GitHubActionsTestLogger/compare/2.3.3...2.4)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b8b4dce7..3ceb087b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + From ce23cdc6444091a324831e9bf16fcc6d4642f6a8 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 3 Jul 2024 15:47:50 -0400 Subject: [PATCH 042/123] feat!: internally maintain provider status (#276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements a few things from spec 0.8.0: - implements internal provider status (already implemented in JS) - the provider no longer updates its status to READY/ERROR, etc after init (the SDK does this automatically) - the provider's state is updated according to the last event it fired - adds `PROVIDER_FATAL` error and code - adds "short circuit" feature when evaluations are skipped if provider is `NOT_READY` or `FATAL` - removes some deprecations that were making the work harder since we already have pending breaking changes. Fixes: https://github.com/open-feature/dotnet-sdk/issues/250 --------- Signed-off-by: Todd Baert Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- src/OpenFeature/Api.cs | 44 ++- src/OpenFeature/Constant/ErrorType.cs | 5 + src/OpenFeature/Constant/ProviderStatus.cs | 7 +- .../Error/ProviderFatalException.cs | 23 ++ .../Error/ProviderNotReadyException.cs | 2 +- src/OpenFeature/EventExecutor.cs | 21 +- src/OpenFeature/FeatureProvider.cs | 24 +- src/OpenFeature/IFeatureClient.cs | 7 + src/OpenFeature/Model/ProviderEvents.cs | 5 + src/OpenFeature/OpenFeatureClient.cs | 18 +- src/OpenFeature/ProviderRepository.cs | 100 +++---- .../OpenFeatureClientTests.cs | 94 +++++- .../OpenFeatureEventTests.cs | 25 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 16 +- .../ProviderRepositoryTests.cs | 269 ++++-------------- test/OpenFeature.Tests/TestImplementations.cs | 43 +-- 16 files changed, 365 insertions(+), 338 deletions(-) create mode 100644 src/OpenFeature/Error/ProviderFatalException.cs diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 6f13cac2..5440151f 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OpenFeature.Constant; +using OpenFeature.Error; using OpenFeature.Model; namespace OpenFeature @@ -37,7 +38,7 @@ static Api() { } private Api() { } /// - /// Sets the feature provider. In order to wait for the provider to be set, and initialization to complete, + /// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete, /// await the returned task. /// /// The provider cannot be set to null. Attempting to set the provider to null has no effect. @@ -45,10 +46,9 @@ private Api() { } public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProviderAsync(featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); } - /// /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and /// initialization to complete, await the returned task. @@ -62,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro throw new ArgumentNullException(nameof(clientName)); } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext()).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); } /// @@ -121,7 +121,7 @@ public FeatureProvider GetProvider(string clientName) /// public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null, EvaluationContext? context = null) => - new FeatureClient(name, version, logger, context); + new FeatureClient(() => _repository.GetProvider(name), name, version, logger, context); /// /// Appends list of hooks to global hooks list @@ -258,6 +258,7 @@ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) public void SetLogger(ILogger logger) { this._eventExecutor.SetLogger(logger); + this._repository.SetLogger(logger); } internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) @@ -265,5 +266,38 @@ internal void AddClientHandler(string client, ProviderEventTypes eventType, Even internal void RemoveClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler) => this._eventExecutor.RemoveClientHandler(client, eventType, handler); + + /// + /// Update the provider state to READY and emit a READY event after successful init. + /// + private async Task AfterInitialization(FeatureProvider provider) + { + provider.Status = ProviderStatus.Ready; + var eventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderReady, + Message = "Provider initialization complete", + ProviderName = provider.GetMetadata().Name, + }; + + await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false); + } + + /// + /// Update the provider state to ERROR and emit an ERROR after failed init. + /// + private async Task AfterError(FeatureProvider provider, Exception ex) + + { + provider.Status = typeof(ProviderFatalException) == ex.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error; + var eventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderError, + Message = $"Provider initialization error: {ex?.Message}", + ProviderName = provider.GetMetadata()?.Name, + }; + + await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false); + } } } diff --git a/src/OpenFeature/Constant/ErrorType.cs b/src/OpenFeature/Constant/ErrorType.cs index 232a57cb..4660e41a 100644 --- a/src/OpenFeature/Constant/ErrorType.cs +++ b/src/OpenFeature/Constant/ErrorType.cs @@ -47,5 +47,10 @@ public enum ErrorType /// Context does not contain a targeting key and the provider requires one. /// [Description("TARGETING_KEY_MISSING")] TargetingKeyMissing, + + /// + /// The provider has entered an irrecoverable error state. + /// + [Description("PROVIDER_FATAL")] ProviderFatal, } } diff --git a/src/OpenFeature/Constant/ProviderStatus.cs b/src/OpenFeature/Constant/ProviderStatus.cs index e56c6c95..16dbd024 100644 --- a/src/OpenFeature/Constant/ProviderStatus.cs +++ b/src/OpenFeature/Constant/ProviderStatus.cs @@ -26,6 +26,11 @@ public enum ProviderStatus /// /// The provider is in an error state and unable to evaluate flags. /// - [Description("ERROR")] Error + [Description("ERROR")] Error, + + /// + /// The provider has entered an irrecoverable error state. + /// + [Description("FATAL")] Fatal, } } diff --git a/src/OpenFeature/Error/ProviderFatalException.cs b/src/OpenFeature/Error/ProviderFatalException.cs new file mode 100644 index 00000000..fae8712a --- /dev/null +++ b/src/OpenFeature/Error/ProviderFatalException.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using OpenFeature.Constant; + +namespace OpenFeature.Error +{ + /// the + /// An exception that signals the provider has entered an irrecoverable error state. + /// + [ExcludeFromCodeCoverage] + public class ProviderFatalException : FeatureProviderException + { + /// + /// Initialize a new instance of the class + /// + /// Exception message + /// Optional inner exception + public ProviderFatalException(string? message = null, Exception? innerException = null) + : base(ErrorType.ProviderFatal, message, innerException) + { + } + } +} diff --git a/src/OpenFeature/Error/ProviderNotReadyException.cs b/src/OpenFeature/Error/ProviderNotReadyException.cs index ca509692..b66201d7 100644 --- a/src/OpenFeature/Error/ProviderNotReadyException.cs +++ b/src/OpenFeature/Error/ProviderNotReadyException.cs @@ -5,7 +5,7 @@ namespace OpenFeature.Error { /// - /// Provider has yet been initialized when evaluating a flag. + /// Provider has not yet been initialized when evaluating a flag. /// [ExcludeFromCodeCoverage] public class ProviderNotReadyException : FeatureProviderException diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 886a47b6..5dfd7dbe 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -184,7 +184,7 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev { return; } - var status = provider.GetStatus(); + var status = provider.Status; var message = ""; if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady) @@ -234,6 +234,7 @@ private async void ProcessFeatureProviderEventsAsync(object? providerRef) switch (item) { case ProviderEventPayload eventPayload: + this.UpdateProviderStatus(typedProviderRef, eventPayload); await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false); break; } @@ -307,6 +308,24 @@ private async void ProcessEventAsync() } } + // map events to provider status as per spec: https://openfeature.dev/specification/sections/events/#requirement-535 + private void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload) + { + switch (eventPayload.Type) + { + case ProviderEventTypes.ProviderReady: + provider.Status = ProviderStatus.Ready; + break; + case ProviderEventTypes.ProviderStale: + provider.Status = ProviderStatus.Stale; + break; + case ProviderEventTypes.ProviderError: + provider.Status = eventPayload.ErrorType == ErrorType.ProviderFatal ? ProviderStatus.Fatal : ProviderStatus.Error; + break; + default: break; + } + } + private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) { try diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 62976f53..32635d95 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -1,10 +1,12 @@ using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required to allow NSubstitute mocking of internal methods namespace OpenFeature { /// @@ -94,22 +96,17 @@ public abstract Task> ResolveStructureValueAsync(string EvaluationContext? context = null, CancellationToken cancellationToken = default); /// - /// Get the status of the provider. + /// Internally-managed provider status. + /// The SDK uses this field to track the status of the provider. + /// Not visible outside OpenFeature assembly /// - /// The current - /// - /// If a provider does not override this method, then its status will be assumed to be - /// . If a provider implements this method, and supports initialization, - /// then it should start in the status . If the status is - /// , then the Api will call the when the - /// provider is set. - /// - public virtual ProviderStatus GetStatus() => ProviderStatus.Ready; + internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady; /// /// /// This method is called before a provider is used to evaluate flags. Providers can overwrite this method, /// if they have special initialization needed prior being called for flag evaluation. + /// When this method completes, the provider will be considered ready for use. /// /// /// @@ -117,12 +114,7 @@ public abstract Task> ResolveStructureValueAsync(string /// A task that completes when the initialization process is complete. /// /// - /// A provider which supports initialization should override this method as well as - /// . - /// - /// - /// The provider should return or from - /// the method after initialization is complete. + /// Providers not implementing this method will be considered ready immediately. /// /// public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) diff --git a/src/OpenFeature/IFeatureClient.cs b/src/OpenFeature/IFeatureClient.cs index 4a09c5e8..f39b7f52 100644 --- a/src/OpenFeature/IFeatureClient.cs +++ b/src/OpenFeature/IFeatureClient.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using OpenFeature.Constant; using OpenFeature.Model; namespace OpenFeature @@ -53,6 +54,12 @@ public interface IFeatureClient : IEventBus /// Client metadata ClientMetadata GetMetadata(); + /// + /// Returns the current status of the associated provider. + /// + /// + ProviderStatus ProviderStatus { get; } + /// /// Resolves a boolean feature flag /// diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index 5c48fc19..bdae057e 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -28,6 +28,11 @@ public class ProviderEventPayload /// public string? Message { get; set; } + /// + /// Optional error associated with the event. + /// + public ErrorType? ErrorType { get; set; } + /// /// A List of flags that have been changed. /// diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 674b78a7..767e8b11 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -21,6 +21,7 @@ public sealed partial class FeatureClient : IFeatureClient private readonly ClientMetadata _metadata; private readonly ConcurrentStack _hooks = new ConcurrentStack(); private readonly ILogger _logger; + private readonly Func _providerAccessor; private EvaluationContext _evaluationContext; private readonly object _evaluationContextLock = new object(); @@ -48,6 +49,9 @@ public sealed partial class FeatureClient : IFeatureClient return (method(provider), provider); } + /// + public ProviderStatus ProviderStatus => this._providerAccessor.Invoke().Status; + /// public EvaluationContext GetContext() { @@ -69,16 +73,18 @@ public void SetContext(EvaluationContext? context) /// /// Initializes a new instance of the class. /// + /// Function to retrieve current provider /// Name of client /// Version of client /// Logger used by client /// Context given to this client /// Throws if any of the required parameters are null - public FeatureClient(string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) + internal FeatureClient(Func providerAccessor, string? name, string? version, ILogger? logger = null, EvaluationContext? context = null) { this._metadata = new ClientMetadata(name, version); this._logger = logger ?? NullLogger.Instance; this._evaluationContext = context ?? EvaluationContext.Empty; + this._providerAccessor = providerAccessor; } /// @@ -246,6 +252,16 @@ private async Task> EvaluateFlagAsync( { var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); + // short circuit evaluation entirely if provider is in a bad state + if (provider.Status == ProviderStatus.NotReady) + { + throw new ProviderNotReadyException("Provider has not yet completed initialization."); + } + else if (provider.Status == ProviderStatus.Fatal) + { + throw new ProviderFatalException("Provider is in an irrecoverable error state."); + } + evaluation = (await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false)) .ToFlagEvaluationDetails(); diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 7934da1c..1656fdd3 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using OpenFeature.Constant; using OpenFeature.Model; @@ -14,6 +16,8 @@ namespace OpenFeature /// internal sealed class ProviderRepository : IAsyncDisposable { + private ILogger _logger; + private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); private readonly ConcurrentDictionary _featureProviders = @@ -31,6 +35,11 @@ internal sealed class ProviderRepository : IAsyncDisposable /// of that provider under different names.. private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); + public ProviderRepository() + { + this._logger = NullLogger.Instance; + } + public async ValueTask DisposeAsync() { using (this._providersLock) @@ -39,36 +48,25 @@ public async ValueTask DisposeAsync() } } + internal void SetLogger(ILogger logger) => this._logger = logger; + /// /// Set the default provider /// /// the provider to set as the default, passing null has no effect /// the context to initialize the provider with - /// - /// - /// Called after the provider is set, but before any actions are taken on it. - /// - /// This can be used for tasks such as registering event handlers. It should be noted that this can be called - /// several times for a single provider. For instance registering a provider with multiple names or as the - /// default and named provider. - /// - /// - /// - /// + /// /// called after the provider has initialized successfully, only called if the provider needed initialization /// - /// + /// /// called if an error happens during the initialization of the provider, only called if the provider needed /// initialization /// - /// called after a provider is shutdown, can be used to remove event handlers public async Task SetProviderAsync( FeatureProvider? featureProvider, EvaluationContext context, - Action? afterSet = null, - Action? afterInitialization = null, - Action? afterError = null, - Action? afterShutdown = null) + Func? afterInitSuccess = null, + Func? afterInitError = null) { // Cannot unset the feature provider. if (featureProvider == null) @@ -88,42 +86,45 @@ public async Task SetProviderAsync( var oldProvider = this._defaultProvider; this._defaultProvider = featureProvider; - afterSet?.Invoke(featureProvider); // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. -#pragma warning disable CS4014 - this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); -#pragma warning restore CS4014 + _ = this.ShutdownIfUnusedAsync(oldProvider); } finally { this._providersLock.ExitWriteLock(); } - await InitProviderAsync(this._defaultProvider, context, afterInitialization, afterError) + await InitProviderAsync(this._defaultProvider, context, afterInitSuccess, afterInitError) .ConfigureAwait(false); } private static async Task InitProviderAsync( FeatureProvider? newProvider, EvaluationContext context, - Action? afterInitialization, - Action? afterError) + Func? afterInitialization, + Func? afterError) { if (newProvider == null) { return; } - if (newProvider.GetStatus() == ProviderStatus.NotReady) + if (newProvider.Status == ProviderStatus.NotReady) { try { await newProvider.InitializeAsync(context).ConfigureAwait(false); - afterInitialization?.Invoke(newProvider); + if (afterInitialization != null) + { + await afterInitialization.Invoke(newProvider).ConfigureAwait(false); + } } catch (Exception ex) { - afterError?.Invoke(newProvider, ex); + if (afterError != null) + { + await afterError.Invoke(newProvider, ex).ConfigureAwait(false); + } } } } @@ -134,32 +135,19 @@ private static async Task InitProviderAsync( /// the name to associate with the provider /// the provider to set as the default, passing null has no effect /// the context to initialize the provider with - /// - /// - /// Called after the provider is set, but before any actions are taken on it. - /// - /// This can be used for tasks such as registering event handlers. It should be noted that this can be called - /// several times for a single provider. For instance registering a provider with multiple names or as the - /// default and named provider. - /// - /// - /// - /// + /// /// called after the provider has initialized successfully, only called if the provider needed initialization /// - /// + /// /// called if an error happens during the initialization of the provider, only called if the provider needed /// initialization /// - /// called after a provider is shutdown, can be used to remove event handlers /// The to cancel any async side effects. - public async Task SetProviderAsync(string clientName, + public async Task SetProviderAsync(string? clientName, FeatureProvider? featureProvider, EvaluationContext context, - Action? afterSet = null, - Action? afterInitialization = null, - Action? afterError = null, - Action? afterShutdown = null, + Func? afterInitSuccess = null, + Func? afterInitError = null, CancellationToken cancellationToken = default) { // Cannot set a provider for a null clientName. @@ -177,7 +165,6 @@ public async Task SetProviderAsync(string clientName, { this._featureProviders.AddOrUpdate(clientName, featureProvider, (key, current) => featureProvider); - afterSet?.Invoke(featureProvider); } else { @@ -188,25 +175,21 @@ public async Task SetProviderAsync(string clientName, // We want to allow shutdown to happen concurrently with initialization, and the caller to not // wait for it. -#pragma warning disable CS4014 - this.ShutdownIfUnusedAsync(oldProvider, afterShutdown, afterError); -#pragma warning restore CS4014 + _ = this.ShutdownIfUnusedAsync(oldProvider); } finally { this._providersLock.ExitWriteLock(); } - await InitProviderAsync(featureProvider, context, afterInitialization, afterError).ConfigureAwait(false); + await InitProviderAsync(featureProvider, context, afterInitSuccess, afterInitError).ConfigureAwait(false); } /// /// Shutdown the feature provider if it is unused. This must be called within a write lock of the _providersLock. /// private async Task ShutdownIfUnusedAsync( - FeatureProvider? targetProvider, - Action? afterShutdown, - Action? afterError) + FeatureProvider? targetProvider) { if (ReferenceEquals(this._defaultProvider, targetProvider)) { @@ -218,7 +201,7 @@ private async Task ShutdownIfUnusedAsync( return; } - await SafeShutdownProviderAsync(targetProvider, afterShutdown, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } /// @@ -230,9 +213,7 @@ private async Task ShutdownIfUnusedAsync( /// it would not be meaningful to emit an error. /// /// - private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider, - Action? afterShutdown, - Action? afterError) + private async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider) { if (targetProvider == null) { @@ -242,11 +223,10 @@ private static async Task SafeShutdownProviderAsync(FeatureProvider? targetProvi try { await targetProvider.ShutdownAsync().ConfigureAwait(false); - afterShutdown?.Invoke(targetProvider); } catch (Exception ex) { - afterError?.Invoke(targetProvider, ex); + this._logger.LogError(ex, $"Error shutting down provider: {targetProvider.GetMetadata().Name}"); } } @@ -307,7 +287,7 @@ public async Task ShutdownAsync(Action? afterError = foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProviderAsync(targetProvider, null, afterError).ConfigureAwait(false); + await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index e7c76d75..925de66a 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -185,6 +185,92 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedLogger.Received(1).IsEnabled(LogLevel.Error); } + [Fact] + [Specification("1.7.3", "The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Ready_If_Init_Succeeds() + { + var name = "1.7.3"; + // provider which succeeds initialization + var provider = new TestProvider(); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be READY + Assert.Equal(ProviderStatus.Ready, client.ProviderStatus); + } + + [Fact] + [Specification("1.7.4", "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Error_If_Init_Fails() + { + var name = "1.7.4"; + // provider which fails initialization + var provider = new TestProvider("some-name", new GeneralException("fake")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be ERROR + Assert.Equal(ProviderStatus.Error, client.ProviderStatus); + } + + [Fact] + [Specification("1.7.5", "The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.")] + [Specification("1.7.1", "The client MUST define a provider status accessor which indicates the readiness of the associated provider, with possible values NOT_READY, READY, STALE, ERROR, or FATAL.")] + public async Task Provider_Status_Should_Be_Fatal_If_Init_Fatal() + { + var name = "1.7.5"; + // provider which fails initialization fatally + var provider = new TestProvider(name, new ProviderFatalException("fatal")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + await Api.Instance.SetProviderAsync(name, provider); + + // after init fails fatally, status should be FATAL + Assert.Equal(ProviderStatus.Fatal, client.ProviderStatus); + } + + [Fact] + [Specification("1.7.6", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] + public async Task Must_Short_Circuit_Not_Ready() + { + var name = "1.7.6"; + var defaultStr = "123-default"; + + // provider which is never ready (ready after maxValue) + var provider = new TestProvider(name, null, int.MaxValue); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + _ = Api.Instance.SetProviderAsync(name, provider); + + var details = await client.GetStringDetailsAsync("some-flag", defaultStr); + Assert.Equal(defaultStr, details.Value); + Assert.Equal(ErrorType.ProviderNotReady, details.ErrorType); + Assert.Equal(Reason.Error, details.Reason); + } + + [Fact] + [Specification("1.7.7", "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in NOT_READY.")] + public async Task Must_Short_Circuit_Fatal() + { + var name = "1.7.6"; + var defaultStr = "456-default"; + + // provider which immediately fails fatally + var provider = new TestProvider(name, new ProviderFatalException("fake")); + FeatureClient client = Api.Instance.GetClient(name); + Assert.Equal(ProviderStatus.NotReady, provider.Status); + _ = Api.Instance.SetProviderAsync(name, provider); + + var details = await client.GetStringDetailsAsync("some-flag", defaultStr); + Assert.Equal(defaultStr, details.Value); + Assert.Equal(ErrorType.ProviderFatal, details.ErrorType); + Assert.Equal(Reason.Error, details.Reason); + } + [Fact] public async Task Should_Resolve_BooleanValue() { @@ -358,15 +444,15 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() var cts = new CancellationTokenSource(); - var featureProviderMock = Substitute.For(); - featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => + var featureProviderMock = Substitute.For(); + featureProviderMock.ResolveStringValueAsync(flagName, defaultString, Arg.Any(), Arg.Any()).Returns(async args => { var token = args.ArgAt(3); - while (!token.IsCancellationRequested) + while (!token.IsCancellationRequested) { await Task.Delay(10); // artificially delay until cancelled } - return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); + return new ResolutionDetails(flagName, defaultString, ErrorType.None, cancelledReason); }); featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index 384928d6..a7bcd2e7 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -147,9 +147,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ var eventHandler = Substitute.For(); var testProvider = new TestProvider(); -#pragma warning disable CS0618// Type or member is obsolete await Api.Instance.SetProviderAsync(testProvider); -#pragma warning restore CS0618// Type or member is obsolete Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); @@ -175,7 +173,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_ var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SetStatus(ProviderStatus.Error); + testProvider.Status = ProviderStatus.Error; Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); @@ -200,7 +198,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_ var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(testProvider); - testProvider.SetStatus(ProviderStatus.Stale); + testProvider.Status = ProviderStatus.Stale; Api.Instance.AddHandler(ProviderEventTypes.ProviderStale, eventHandler); @@ -476,7 +474,11 @@ public async Task Client_Level_Event_Handlers_Should_Be_Removable() await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); // wait for the first event to be received - await Utils.AssertUntilAsync(_ => myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler)); + await Utils.AssertUntilAsync( + _ => eventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)) + ); + + myClient.RemoveHandler(ProviderEventTypes.ProviderReady, eventHandler); // send another event from the provider - this one should not be received await testProvider.SendEventAsync(ProviderEventTypes.ProviderReady); @@ -501,5 +503,18 @@ public void RegisterClientFeatureProvider_WhenCalledWithNullProvider_DoesNotThro // Assert Assert.Null(exception); } + + [Theory] + [InlineData(ProviderEventTypes.ProviderError, ProviderStatus.Error)] + [InlineData(ProviderEventTypes.ProviderReady, ProviderStatus.Ready)] + [InlineData(ProviderEventTypes.ProviderStale, ProviderStatus.Stale)] + [Specification("5.3.5", "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.")] + public async Task Provider_Events_Should_Update_ProviderStatus(ProviderEventTypes type, ProviderStatus status) + { + var provider = new TestProvider(); + await Api.Instance.SetProviderAsync("5.3.5", provider); + _ = provider.SendEventAsync(type); + await Utils.AssertUntilAsync(_ => Assert.True(provider.Status == status)); + } } } diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 673c183d..1df3c976 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -28,13 +28,13 @@ public void OpenFeature_Should_Be_Singleton() public async Task OpenFeature_Should_Initialize_Provider() { var providerMockDefault = Substitute.For(); - providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady); + providerMockDefault.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerMockDefault); await providerMockDefault.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerMockNamed = Substitute.For(); - providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady); + providerMockNamed.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("the-name", providerMockNamed); await providerMockNamed.Received(1).InitializeAsync(Api.Instance.GetContext()); @@ -46,26 +46,26 @@ public async Task OpenFeature_Should_Initialize_Provider() public async Task OpenFeature_Should_Shutdown_Unused_Provider() { var providerA = Substitute.For(); - providerA.GetStatus().Returns(ProviderStatus.NotReady); + providerA.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerA); await providerA.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerB = Substitute.For(); - providerB.GetStatus().Returns(ProviderStatus.NotReady); + providerB.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerB); await providerB.Received(1).InitializeAsync(Api.Instance.GetContext()); await providerA.Received(1).ShutdownAsync(); var providerC = Substitute.For(); - providerC.GetStatus().Returns(ProviderStatus.NotReady); + providerC.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("named", providerC); await providerC.Received(1).InitializeAsync(Api.Instance.GetContext()); var providerD = Substitute.For(); - providerD.GetStatus().Returns(ProviderStatus.NotReady); + providerD.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync("named", providerD); await providerD.Received(1).InitializeAsync(Api.Instance.GetContext()); @@ -77,10 +77,10 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider() public async Task OpenFeature_Should_Support_Shutdown() { var providerA = Substitute.For(); - providerA.GetStatus().Returns(ProviderStatus.NotReady); + providerA.Status.Returns(ProviderStatus.NotReady); var providerB = Substitute.For(); - providerB.GetStatus().Returns(ProviderStatus.NotReady); + providerB.Status.Returns(ProviderStatus.NotReady); await Api.Instance.SetProviderAsync(providerA); await Api.Instance.SetProviderAsync("named", providerB); diff --git a/test/OpenFeature.Tests/ProviderRepositoryTests.cs b/test/OpenFeature.Tests/ProviderRepositoryTests.cs index ccec89bd..e88de6e9 100644 --- a/test/OpenFeature.Tests/ProviderRepositoryTests.cs +++ b/test/OpenFeature.Tests/ProviderRepositoryTests.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using NSubstitute; -using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; using OpenFeature.Model; using Xunit; @@ -25,28 +24,12 @@ public async Task Default_Provider_Is_Set_Without_Await() Assert.Equal(provider, repository.GetProvider()); } - [Fact] - public async Task AfterSet_Is_Invoked_For_Setting_Default_Provider() - { - var repository = new ProviderRepository(); - var provider = new NoOpFeatureProvider(); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProviderAsync(provider, context, afterSet: (theProvider) => - { - callCount++; - Assert.Equal(provider, theProvider); - }); - Assert.Equal(1, callCount); - } - [Fact] public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(providerMock, context); providerMock.Received(1).InitializeAsync(context); @@ -58,13 +41,14 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Default_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProviderAsync(providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync(providerMock, context, afterInitSuccess: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; + return Task.CompletedTask; }); Assert.Equal(1, callCount); } @@ -74,16 +58,17 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Default_Provide { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProviderAsync(providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync(providerMock, context, afterInitError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; + return Task.CompletedTask; }); Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); @@ -93,11 +78,11 @@ await repository.SetProviderAsync(providerMock, context, afterError: (theProvide [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + internal async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(providerMock, context); providerMock.DidNotReceive().InitializeAsync(context); @@ -107,14 +92,18 @@ public async Task Initialize_Is_Not_Called_For_Ready_Provider(ProviderStatus sta [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) + internal async Task AfterInitialize_Is_Not_Called_For_Ready_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProviderAsync(providerMock, context, afterInitialization: provider => { callCount++; }); + await repository.SetProviderAsync(providerMock, context, afterInitSuccess: provider => + { + callCount++; + return Task.CompletedTask; + }); Assert.Equal(0, callCount); } @@ -123,10 +112,10 @@ public async Task Replaced_Default_Provider_Is_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(provider1, context); @@ -135,52 +124,6 @@ public async Task Replaced_Default_Provider_Is_Shutdown() provider2.DidNotReceive().ShutdownAsync(); } - [Fact] - public async Task AfterShutdown_Is_Called_For_Shutdown_Provider() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider1, context); - var callCount = 0; - await repository.SetProviderAsync(provider2, context, afterShutdown: provider => - { - Assert.Equal(provider, provider1); - callCount++; - }); - Assert.Equal(1, callCount); - } - - [Fact] - public async Task AfterError_Is_Called_For_Shutdown_That_Throws() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync(provider1, context); - var callCount = 0; - Exception? errorThrown = null; - await repository.SetProviderAsync(provider2, context, afterError: (provider, ex) => - { - Assert.Equal(provider, provider1); - errorThrown = ex; - callCount++; - }); - Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); - } - [Fact] public async Task Named_Provider_Provider_Is_Set_Without_Await() { @@ -192,28 +135,12 @@ public async Task Named_Provider_Provider_Is_Set_Without_Await() Assert.Equal(provider, repository.GetProvider("the-name")); } - [Fact] - public async Task AfterSet_Is_Invoked_For_Setting_Named_Provider() - { - var repository = new ProviderRepository(); - var provider = new NoOpFeatureProvider(); - var context = new EvaluationContextBuilder().Build(); - var callCount = 0; - // The setting of the provider is synchronous, so the afterSet should be as well. - await repository.SetProviderAsync("the-name", provider, context, afterSet: (theProvider) => - { - callCount++; - Assert.Equal(provider, theProvider); - }); - Assert.Equal(1, callCount); - } - [Fact] public async Task Initialization_Provider_Method_Is_Invoked_For_Setting_Named_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", providerMock, context); providerMock.Received(1).InitializeAsync(context); @@ -225,13 +152,14 @@ public async Task AfterInitialization_Is_Invoked_For_Setting_Named_Provider() { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); var callCount = 0; - await repository.SetProviderAsync("the-name", providerMock, context, afterInitialization: (theProvider) => + await repository.SetProviderAsync("the-name", providerMock, context, afterInitSuccess: (theProvider) => { Assert.Equal(providerMock, theProvider); callCount++; + return Task.CompletedTask; }); Assert.Equal(1, callCount); } @@ -241,16 +169,17 @@ public async Task AfterError_Is_Invoked_If_Initialization_Errors_Named_Provider( { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(ProviderStatus.NotReady); + providerMock.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); providerMock.When(x => x.InitializeAsync(context)).Throw(new Exception("BAD THINGS")); var callCount = 0; Exception? receivedError = null; - await repository.SetProviderAsync("the-provider", providerMock, context, afterError: (theProvider, error) => + await repository.SetProviderAsync("the-provider", providerMock, context, afterInitError: (theProvider, error) => { Assert.Equal(providerMock, theProvider); callCount++; receivedError = error; + return Task.CompletedTask; }); Assert.Equal("BAD THINGS", receivedError?.Message); Assert.Equal(1, callCount); @@ -260,11 +189,11 @@ await repository.SetProviderAsync("the-provider", providerMock, context, afterEr [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + internal async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", providerMock, context); providerMock.DidNotReceive().InitializeAsync(context); @@ -274,15 +203,19 @@ public async Task Initialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStat [InlineData(ProviderStatus.Ready)] [InlineData(ProviderStatus.Stale)] [InlineData(ProviderStatus.Error)] - public async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) + internal async Task AfterInitialize_Is_Not_Called_For_Ready_Named_Provider(ProviderStatus status) { var repository = new ProviderRepository(); var providerMock = Substitute.For(); - providerMock.GetStatus().Returns(status); + providerMock.Status.Returns(status); var context = new EvaluationContextBuilder().Build(); var callCount = 0; await repository.SetProviderAsync("the-name", providerMock, context, - afterInitialization: provider => { callCount++; }); + afterInitSuccess: provider => + { + callCount++; + return Task.CompletedTask; + }); Assert.Equal(0, callCount); } @@ -291,10 +224,10 @@ public async Task Replaced_Named_Provider_Is_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync("the-name", provider1, context); @@ -303,61 +236,15 @@ public async Task Replaced_Named_Provider_Is_Shutdown() provider2.DidNotReceive().ShutdownAsync(); } - [Fact] - public async Task AfterShutdown_Is_Called_For_Shutdown_Named_Provider() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-provider", provider1, context); - var callCount = 0; - await repository.SetProviderAsync("the-provider", provider2, context, afterShutdown: provider => - { - Assert.Equal(provider, provider1); - callCount++; - }); - Assert.Equal(1, callCount); - } - - [Fact] - public async Task AfterError_Is_Called_For_Shutdown_Named_Provider_That_Throws() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR")); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - await repository.SetProviderAsync("the-name", provider1, context); - var callCount = 0; - Exception? errorThrown = null; - await repository.SetProviderAsync("the-name", provider2, context, afterError: (provider, ex) => - { - Assert.Equal(provider, provider1); - errorThrown = ex; - callCount++; - }); - Assert.Equal(1, callCount); - Assert.Equal("SHUTDOWN ERROR", errorThrown?.Message); - } - [Fact] public async Task In_Use_Provider_Named_And_Default_Is_Not_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -374,10 +261,10 @@ public async Task In_Use_Provider_Two_Named_Is_Not_Shutdown() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -394,10 +281,10 @@ public async Task When_All_Instances_Are_Removed_Shutdown_Is_Called() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -415,10 +302,10 @@ public async Task Can_Get_Providers_By_Name() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -434,10 +321,10 @@ public async Task Replaced_Named_Provider_Gets_Latest_Set() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -452,13 +339,13 @@ public async Task Can_Shutdown_All_Providers() { var repository = new ProviderRepository(); var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); + provider1.Status.Returns(ProviderStatus.NotReady); var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); + provider2.Status.Returns(ProviderStatus.NotReady); var provider3 = Substitute.For(); - provider3.GetStatus().Returns(ProviderStatus.NotReady); + provider3.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); @@ -475,62 +362,12 @@ public async Task Can_Shutdown_All_Providers() provider3.Received(1).ShutdownAsync(); } - [Fact] - public async Task Errors_During_Shutdown_Propagate() - { - var repository = new ProviderRepository(); - var provider1 = Substitute.For(); - provider1.GetStatus().Returns(ProviderStatus.NotReady); - provider1.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 1")); - - var provider2 = Substitute.For(); - provider2.GetStatus().Returns(ProviderStatus.NotReady); - provider2.ShutdownAsync().Throws(new Exception("SHUTDOWN ERROR 2")); - - var provider3 = Substitute.For(); - provider3.GetStatus().Returns(ProviderStatus.NotReady); - - var context = new EvaluationContextBuilder().Build(); - - await repository.SetProviderAsync(provider1, context); - await repository.SetProviderAsync("provider1", provider1, context); - await repository.SetProviderAsync("provider2", provider2, context); - await repository.SetProviderAsync("provider2a", provider2, context); - await repository.SetProviderAsync("provider3", provider3, context); - - var callCountShutdown1 = 0; - var callCountShutdown2 = 0; - var totalCallCount = 0; - await repository.ShutdownAsync(afterError: (provider, exception) => - { - totalCallCount++; - if (provider == provider1) - { - callCountShutdown1++; - Assert.Equal("SHUTDOWN ERROR 1", exception.Message); - } - - if (provider == provider2) - { - callCountShutdown2++; - Assert.Equal("SHUTDOWN ERROR 2", exception.Message); - } - }); - Assert.Equal(2, totalCallCount); - Assert.Equal(1, callCountShutdown1); - Assert.Equal(1, callCountShutdown2); - - provider1.Received(1).ShutdownAsync(); - provider2.Received(1).ShutdownAsync(); - provider3.Received(1).ShutdownAsync(); - } - [Fact] public async Task Setting_Same_Default_Provider_Has_No_Effect() { var repository = new ProviderRepository(); var provider = Substitute.For(); - provider.GetStatus().Returns(ProviderStatus.NotReady); + provider.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(provider, context); await repository.SetProviderAsync(provider, context); @@ -545,7 +382,7 @@ public async Task Setting_Null_Default_Provider_Has_No_Effect() { var repository = new ProviderRepository(); var provider = Substitute.For(); - provider.GetStatus().Returns(ProviderStatus.NotReady); + provider.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(provider, context); await repository.SetProviderAsync(null, context); @@ -561,10 +398,10 @@ public async Task Setting_Null_Named_Provider_Removes_It() var repository = new ProviderRepository(); var namedProvider = Substitute.For(); - namedProvider.GetStatus().Returns(ProviderStatus.NotReady); + namedProvider.Status.Returns(ProviderStatus.NotReady); var defaultProvider = Substitute.For(); - defaultProvider.GetStatus().Returns(ProviderStatus.NotReady); + defaultProvider.Status.Returns(ProviderStatus.NotReady); var context = new EvaluationContextBuilder().Build(); await repository.SetProviderAsync(defaultProvider, context); diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index c949b373..a4fe51a4 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -40,24 +40,30 @@ public class TestProvider : FeatureProvider public static string DefaultName = "test-provider"; - public string Name { get; set; } - - private ProviderStatus _status; + public string? Name { get; set; } public void AddHook(Hook hook) => this._hooks.Add(hook); public override IImmutableList GetProviderHooks() => this._hooks.ToImmutableList(); + private Exception? initException = null; + private int initDelay = 0; public TestProvider() { - this._status = ProviderStatus.NotReady; this.Name = DefaultName; } - public TestProvider(string name) + /// + /// A provider used for testing. + /// + /// the name of the provider. + /// Optional exception to throw during init. + /// + public TestProvider(string? name, Exception? initException = null, int initDelay = 0) { - this._status = ProviderStatus.NotReady; - this.Name = name; + this.Name = string.IsNullOrEmpty(name) ? DefaultName : name; + this.initException = initException; + this.initDelay = initDelay; } public override Metadata GetMetadata() @@ -95,26 +101,23 @@ public override Task> ResolveStructureValueAsync(string return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); } - public override ProviderStatus GetStatus() - { - return this._status; - } - - public void SetStatus(ProviderStatus status) + public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { - this._status = status; + await Task.Delay(initDelay).ConfigureAwait(false); + if (initException != null) + { + throw initException; + } } - public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) { - this._status = ProviderStatus.Ready; - await this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady, ProviderName = this.GetMetadata().Name }, cancellationToken).ConfigureAwait(false); - await base.InitializeAsync(context, cancellationToken).ConfigureAwait(false); + return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name, }, cancellationToken); } - internal ValueTask SendEventAsync(ProviderEventTypes eventType, CancellationToken cancellationToken = default) + internal ValueTask SendEventAsync(ProviderEventPayload payload, CancellationToken cancellationToken = default) { - return this.EventChannel.Writer.WriteAsync(new ProviderEventPayload { Type = eventType, ProviderName = this.GetMetadata().Name }, cancellationToken); + return this.EventChannel.Writer.WriteAsync(payload, cancellationToken); } } } From 7716f4a568d9308ff44c4b94e8408ce820250183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:19:10 +0100 Subject: [PATCH 043/123] chore: cleanup code (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR This PR fixes some of the warnings and typos that I recently found. More interestingly, it addresses these issues: - Missing the `.this` - Usage of `ILogger` vs `Source generator log` - `const` vs `static` - Fix nullability for some methods and properties. And a few more changes. ### Follow-up Tasks We need to do more cleanup tasks. ### How to test All of these changes are recommended by the IDE and "tested" by the compiler when it executes. --------- Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- src/OpenFeature/Api.cs | 19 ++++--- src/OpenFeature/Constant/Reason.cs | 16 +++--- .../Error/ProviderFatalException.cs | 2 +- src/OpenFeature/EventExecutor.cs | 3 +- src/OpenFeature/FeatureProvider.cs | 8 +-- .../Model/EvaluationContextBuilder.cs | 4 +- .../Model/FlagEvaluationOptions.cs | 4 +- src/OpenFeature/Model/ImmutableMetadata.cs | 1 - src/OpenFeature/Model/Value.cs | 18 +++---- src/OpenFeature/OpenFeatureClient.cs | 15 +++--- src/OpenFeature/ProviderRepository.cs | 26 +++++----- src/OpenFeature/Providers/Memory/Flag.cs | 31 +++++------ .../Providers/Memory/InMemoryProvider.cs | 30 +++++------ .../OpenFeatureClientBenchmarks.cs | 52 +++++++++---------- .../Steps/EvaluationStepDefinitions.cs | 18 +++---- test/OpenFeature.Tests/OpenFeatureTests.cs | 10 ++-- test/OpenFeature.Tests/TestImplementations.cs | 6 +-- 17 files changed, 125 insertions(+), 138 deletions(-) diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 5440151f..3fa38916 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -32,7 +32,7 @@ public sealed class Api : IEventBus public static Api Instance { get; } = new Api(); // Explicit static constructor to tell C# compiler - // not to mark type as beforefieldinit + // not to mark type as beforeFieldInit // IE Lazy way of ensuring this is thread safe without using locks static Api() { } private Api() { } @@ -46,7 +46,7 @@ private Api() { } public async Task SetProviderAsync(FeatureProvider featureProvider) { this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider); - await this._repository.SetProviderAsync(featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); + await this._repository.SetProviderAsync(featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); } /// @@ -62,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro throw new ArgumentNullException(nameof(clientName)); } this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false); + await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); } /// @@ -101,7 +101,7 @@ public FeatureProvider GetProvider(string clientName) /// /// /// - public Metadata GetProviderMetadata() => this.GetProvider().GetMetadata(); + public Metadata? GetProviderMetadata() => this.GetProvider().GetMetadata(); /// /// Gets providers metadata assigned to the given clientName. If the clientName has no provider @@ -109,7 +109,7 @@ public FeatureProvider GetProvider(string clientName) /// /// Name of client /// Metadata assigned to provider - public Metadata GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); + public Metadata? GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); /// /// Create a new instance of using the current provider @@ -121,7 +121,7 @@ public FeatureProvider GetProvider(string clientName) /// public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null, EvaluationContext? context = null) => - new FeatureClient(() => _repository.GetProvider(name), name, version, logger, context); + new FeatureClient(() => this._repository.GetProvider(name), name, version, logger, context); /// /// Appends list of hooks to global hooks list @@ -277,7 +277,7 @@ private async Task AfterInitialization(FeatureProvider provider) { Type = ProviderEventTypes.ProviderReady, Message = "Provider initialization complete", - ProviderName = provider.GetMetadata().Name, + ProviderName = provider.GetMetadata()?.Name, }; await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false); @@ -286,10 +286,9 @@ private async Task AfterInitialization(FeatureProvider provider) /// /// Update the provider state to ERROR and emit an ERROR after failed init. /// - private async Task AfterError(FeatureProvider provider, Exception ex) - + private async Task AfterError(FeatureProvider provider, Exception? ex) { - provider.Status = typeof(ProviderFatalException) == ex.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error; + provider.Status = typeof(ProviderFatalException) == ex?.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error; var eventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderError, diff --git a/src/OpenFeature/Constant/Reason.cs b/src/OpenFeature/Constant/Reason.cs index a60ce78a..eac06c1e 100644 --- a/src/OpenFeature/Constant/Reason.cs +++ b/src/OpenFeature/Constant/Reason.cs @@ -9,42 +9,42 @@ public static class Reason /// /// Use when the flag is matched based on the evaluation context user data /// - public static string TargetingMatch = "TARGETING_MATCH"; + public const string TargetingMatch = "TARGETING_MATCH"; /// /// Use when the flag is matched based on a split rule in the feature flag provider /// - public static string Split = "SPLIT"; + public const string Split = "SPLIT"; /// /// Use when the flag is disabled in the feature flag provider /// - public static string Disabled = "DISABLED"; + public const string Disabled = "DISABLED"; /// /// Default reason when evaluating flag /// - public static string Default = "DEFAULT"; + public const string Default = "DEFAULT"; /// /// The resolved value is static (no dynamic evaluation) /// - public static string Static = "STATIC"; + public const string Static = "STATIC"; /// /// The resolved value was retrieved from cache /// - public static string Cached = "CACHED"; + public const string Cached = "CACHED"; /// /// Use when an unknown reason is encountered when evaluating flag. /// An example of this is if the feature provider returns a reason that is not defined in the spec /// - public static string Unknown = "UNKNOWN"; + public const string Unknown = "UNKNOWN"; /// /// Use this flag when abnormal execution is encountered. /// - public static string Error = "ERROR"; + public const string Error = "ERROR"; } } diff --git a/src/OpenFeature/Error/ProviderFatalException.cs b/src/OpenFeature/Error/ProviderFatalException.cs index fae8712a..894a583d 100644 --- a/src/OpenFeature/Error/ProviderFatalException.cs +++ b/src/OpenFeature/Error/ProviderFatalException.cs @@ -4,7 +4,7 @@ namespace OpenFeature.Error { - /// the + /// /// An exception that signals the provider has entered an irrecoverable error state. /// [ExcludeFromCodeCoverage] diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 5dfd7dbe..ad53a949 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -206,7 +206,7 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev { handler.Invoke(new ProviderEventPayload { - ProviderName = provider.GetMetadata().Name, + ProviderName = provider.GetMetadata()?.Name, Type = eventType, Message = message }); @@ -322,6 +322,7 @@ private void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload case ProviderEventTypes.ProviderError: provider.Status = eventPayload.ErrorType == ErrorType.ProviderFatal ? ProviderStatus.Fatal : ProviderStatus.Error; break; + case ProviderEventTypes.ProviderConfigurationChanged: default: break; } } diff --git a/src/OpenFeature/FeatureProvider.cs b/src/OpenFeature/FeatureProvider.cs index 32635d95..c4ce8783 100644 --- a/src/OpenFeature/FeatureProvider.cs +++ b/src/OpenFeature/FeatureProvider.cs @@ -11,14 +11,14 @@ namespace OpenFeature { /// /// The provider interface describes the abstraction layer for a feature flag provider. - /// A provider acts as the translates layer between the generic feature flag structure to a target feature flag system. + /// A provider acts as it translates layer between the generic feature flag structure to a target feature flag system. /// /// Provider specification public abstract class FeatureProvider { /// - /// Gets a immutable list of hooks that belong to the provider. - /// By default return a empty list + /// Gets an immutable list of hooks that belong to the provider. + /// By default, return an empty list /// /// Executed in the order of hooks /// before: API, Client, Invocation, Provider @@ -38,7 +38,7 @@ public abstract class FeatureProvider /// Metadata describing the provider. /// /// - public abstract Metadata GetMetadata(); + public abstract Metadata? GetMetadata(); /// /// Resolves a boolean feature flag diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index 1afb02fc..c672c401 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -140,9 +140,9 @@ public EvaluationContextBuilder Merge(EvaluationContext context) { string? newTargetingKey = ""; - if (!string.IsNullOrWhiteSpace(TargetingKey)) + if (!string.IsNullOrWhiteSpace(this.TargetingKey)) { - newTargetingKey = TargetingKey; + newTargetingKey = this.TargetingKey; } if (!string.IsNullOrWhiteSpace(context.TargetingKey)) diff --git a/src/OpenFeature/Model/FlagEvaluationOptions.cs b/src/OpenFeature/Model/FlagEvaluationOptions.cs index 7bde600c..8bba0aef 100644 --- a/src/OpenFeature/Model/FlagEvaluationOptions.cs +++ b/src/OpenFeature/Model/FlagEvaluationOptions.cs @@ -10,12 +10,12 @@ namespace OpenFeature.Model public sealed class FlagEvaluationOptions { /// - /// A immutable list of + /// An immutable list of /// public IImmutableList Hooks { get; } /// - /// A immutable dictionary of hook hints + /// An immutable dictionary of hook hints /// public IImmutableDictionary HookHints { get; } diff --git a/src/OpenFeature/Model/ImmutableMetadata.cs b/src/OpenFeature/Model/ImmutableMetadata.cs index 40d452d0..1f2c6f8a 100644 --- a/src/OpenFeature/Model/ImmutableMetadata.cs +++ b/src/OpenFeature/Model/ImmutableMetadata.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; -#nullable enable namespace OpenFeature.Model; /// diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs index 5af3b8b3..88fb0734 100644 --- a/src/OpenFeature/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -139,49 +139,49 @@ public Value(Object value) public object? AsObject => this._innerValue; /// - /// Returns the underlying int value - /// Value will be null if it isn't a integer + /// Returns the underlying int value. + /// Value will be null if it isn't an integer /// /// Value as int - public int? AsInteger => this.IsNumber ? (int?)Convert.ToInt32((double?)this._innerValue) : null; + public int? AsInteger => this.IsNumber ? Convert.ToInt32((double?)this._innerValue) : null; /// - /// Returns the underlying bool value + /// Returns the underlying bool value. /// Value will be null if it isn't a bool /// /// Value as bool public bool? AsBoolean => this.IsBoolean ? (bool?)this._innerValue : null; /// - /// Returns the underlying double value + /// Returns the underlying double value. /// Value will be null if it isn't a double /// /// Value as int public double? AsDouble => this.IsNumber ? (double?)this._innerValue : null; /// - /// Returns the underlying string value + /// Returns the underlying string value. /// Value will be null if it isn't a string /// /// Value as string public string? AsString => this.IsString ? (string?)this._innerValue : null; /// - /// Returns the underlying Structure value + /// Returns the underlying Structure value. /// Value will be null if it isn't a Structure /// /// Value as Structure public Structure? AsStructure => this.IsStructure ? (Structure?)this._innerValue : null; /// - /// Returns the underlying List value + /// Returns the underlying List value. /// Value will be null if it isn't a List /// /// Value as List public IImmutableList? AsList => this.IsList ? (IImmutableList?)this._innerValue : null; /// - /// Returns the underlying DateTime value + /// Returns the underlying DateTime value. /// Value will be null if it isn't a DateTime /// /// Value as DateTime diff --git a/src/OpenFeature/OpenFeatureClient.cs b/src/OpenFeature/OpenFeatureClient.cs index 767e8b11..08e29533 100644 --- a/src/OpenFeature/OpenFeatureClient.cs +++ b/src/OpenFeature/OpenFeatureClient.cs @@ -212,11 +212,8 @@ private async Task> EvaluateFlagAsync( var resolveValueDelegate = providerInfo.Item1; var provider = providerInfo.Item2; - // New up a evaluation context if one was not provided. - if (context == null) - { - context = EvaluationContext.Empty; - } + // New up an evaluation context if one was not provided. + context ??= EvaluationContext.Empty; // merge api, client, and invocation context. var evaluationContext = Api.Instance.GetContext(); @@ -253,11 +250,11 @@ private async Task> EvaluateFlagAsync( var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false); // short circuit evaluation entirely if provider is in a bad state - if (provider.Status == ProviderStatus.NotReady) + if (provider.Status == ProviderStatus.NotReady) { throw new ProviderNotReadyException("Provider has not yet completed initialization."); - } - else if (provider.Status == ProviderStatus.Fatal) + } + else if (provider.Status == ProviderStatus.Fatal) { throw new ProviderFatalException("Provider is in an irrecoverable error state."); } @@ -349,7 +346,7 @@ private async Task TriggerFinallyHooksAsync(IReadOnlyList hooks, HookCo } catch (Exception e) { - this._logger.LogError(e, "Error while executing Finally hook {HookName}", hook.GetType().Name); + this.FinallyHookError(hook.GetType().Name, e); } } } diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 1656fdd3..760503b6 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -14,9 +14,9 @@ namespace OpenFeature /// /// This class manages the collection of providers, both default and named, contained by the API. /// - internal sealed class ProviderRepository : IAsyncDisposable + internal sealed partial class ProviderRepository : IAsyncDisposable { - private ILogger _logger; + private ILogger _logger = NullLogger.Instance; private FeatureProvider _defaultProvider = new NoOpFeatureProvider(); @@ -26,20 +26,15 @@ internal sealed class ProviderRepository : IAsyncDisposable /// The reader/writer locks is not disposed because the singleton instance should never be disposed. /// /// Mutations of the _defaultProvider or _featureProviders are done within this lock even though - /// _featureProvider is a concurrent collection. This is for a couple reasons, the first is that + /// _featureProvider is a concurrent collection. This is for a couple of reasons, the first is that /// a provider should only be shutdown if it is not in use, and it could be in use as either a named or /// default provider. /// - /// The second is that a concurrent collection doesn't provide any ordering so we could check a provider + /// The second is that a concurrent collection doesn't provide any ordering, so we could check a provider /// as it was being added or removed such as two concurrent calls to SetProvider replacing multiple instances - /// of that provider under different names.. + /// of that provider under different names. private readonly ReaderWriterLockSlim _providersLock = new ReaderWriterLockSlim(); - public ProviderRepository() - { - this._logger = NullLogger.Instance; - } - public async ValueTask DisposeAsync() { using (this._providersLock) @@ -201,7 +196,7 @@ private async Task ShutdownIfUnusedAsync( return; } - await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } /// @@ -209,7 +204,7 @@ private async Task ShutdownIfUnusedAsync( /// Shut down the provider and capture any exceptions thrown. /// /// - /// The provider is set either to a name or default before the old provider it shutdown, so + /// The provider is set either to a name or default before the old provider it shut down, so /// it would not be meaningful to emit an error. /// /// @@ -226,7 +221,7 @@ private async Task SafeShutdownProviderAsync(FeatureProvider? targetProvider) } catch (Exception ex) { - this._logger.LogError(ex, $"Error shutting down provider: {targetProvider.GetMetadata().Name}"); + this.ErrorShuttingDownProvider(targetProvider.GetMetadata()?.Name, ex); } } @@ -287,8 +282,11 @@ public async Task ShutdownAsync(Action? afterError = foreach (var targetProvider in providers) { // We don't need to take any actions after shutdown. - await SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); + await this.SafeShutdownProviderAsync(targetProvider).ConfigureAwait(false); } } + + [LoggerMessage(EventId = 105, Level = LogLevel.Error, Message = "Error shutting down provider: {TargetProviderName}`")] + partial void ErrorShuttingDownProvider(string? targetProviderName, Exception exception); } } diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 1a16bfe3..5cee86ea 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -9,19 +9,16 @@ namespace OpenFeature.Providers.Memory /// /// Flag representation for the in-memory provider. /// - public interface Flag - { - - } + public interface Flag; /// /// Flag representation for the in-memory provider. /// public sealed class Flag : Flag { - private Dictionary Variants; - private string DefaultVariant; - private Func? ContextEvaluator; + private readonly Dictionary _variants; + private readonly string _defaultVariant; + private readonly Func? _contextEvaluator; /// /// Flag representation for the in-memory provider. @@ -31,34 +28,34 @@ public sealed class Flag : Flag /// optional context-sensitive evaluation function public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null) { - this.Variants = variants; - this.DefaultVariant = defaultVariant; - this.ContextEvaluator = contextEvaluator; + this._variants = variants; + this._defaultVariant = defaultVariant; + this._contextEvaluator = contextEvaluator; } internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) { - T? value = default; - if (this.ContextEvaluator == null) + T? value; + if (this._contextEvaluator == null) { - if (this.Variants.TryGetValue(this.DefaultVariant, out value)) + if (this._variants.TryGetValue(this._defaultVariant, out value)) { return new ResolutionDetails( flagKey, value, - variant: this.DefaultVariant, + variant: this._defaultVariant, reason: Reason.Static ); } else { - throw new GeneralException($"variant {this.DefaultVariant} not found"); + throw new GeneralException($"variant {this._defaultVariant} not found"); } } else { - var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); - if (!this.Variants.TryGetValue(variant, out value)) + var variant = this._contextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); + if (!this._variants.TryGetValue(variant, out value)) { throw new GeneralException($"variant {variant} not found"); } diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index e56acdb5..771e2210 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -61,7 +61,7 @@ public async Task UpdateFlags(IDictionary? flags = null) var @event = new ProviderEventPayload { Type = ProviderEventTypes.ProviderConfigurationChanged, - ProviderName = _metadata.Name, + ProviderName = this._metadata.Name, FlagsChanged = changed, // emit all Message = "flags changed", }; @@ -71,31 +71,31 @@ public async Task UpdateFlags(IDictionary? flags = null) /// public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } /// public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) { - return Task.FromResult(Resolve(flagKey, defaultValue, context)); + return Task.FromResult(this.Resolve(flagKey, defaultValue, context)); } private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context) @@ -104,19 +104,15 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati { throw new FlagNotFoundException($"flag {flagKey} not found"); } - else + + // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. + // In a production provider, such behavior is probably not desirable; consider supporting conversion. + if (flag is Flag value) { - // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. - // In a production provider, such behavior is probably not desirable; consider supporting conversion. - if (typeof(Flag).Equals(flag.GetType())) - { - return ((Flag)flag).Evaluate(flagKey, defaultValue, context); - } - else - { - throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}"); - } + return value.Evaluate(flagKey, defaultValue, context); } + + throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}"); } } } diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 7f2e5b30..3796821e 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -30,77 +30,77 @@ public class OpenFeatureClientBenchmarks public OpenFeatureClientBenchmarks() { var fixture = new Fixture(); - _clientName = fixture.Create(); - _clientVersion = fixture.Create(); - _flagName = fixture.Create(); - _defaultBoolValue = fixture.Create(); - _defaultStringValue = fixture.Create(); - _defaultIntegerValue = fixture.Create(); - _defaultDoubleValue = fixture.Create(); - _defaultStructureValue = fixture.Create(); - _emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - - _client = Api.Instance.GetClient(_clientName, _clientVersion); + this._clientName = fixture.Create(); + this._clientVersion = fixture.Create(); + this._flagName = fixture.Create(); + this._defaultBoolValue = fixture.Create(); + this._defaultStringValue = fixture.Create(); + this._defaultIntegerValue = fixture.Create(); + this._defaultDoubleValue = fixture.Create(); + this._defaultStructureValue = fixture.Create(); + this._emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); + + this._client = Api.Instance.GetClient(this._clientName, this._clientVersion); } [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue); + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty); + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetBooleanValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetBooleanValueAsync(_flagName, _defaultBoolValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetBooleanValueAsync(this._flagName, this._defaultBoolValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValueAsync(_flagName, _defaultStringValue); + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty); + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetStringValue_WithoutEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetStringValueAsync(_flagName, _defaultStringValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetStringValueAsync(this._flagName, this._defaultStringValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue); + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty); + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetIntegerValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetIntegerValueAsync(_flagName, _defaultIntegerValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetIntegerValueAsync(this._flagName, this._defaultIntegerValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue); + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty); + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetDoubleValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetDoubleValueAsync(_flagName, _defaultDoubleValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetDoubleValueAsync(this._flagName, this._defaultDoubleValue, EvaluationContext.Empty, this._emptyFlagOptions); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithoutEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValueAsync(_flagName, _defaultStructureValue); + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithoutFlagEvaluationOptions() => - await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty); + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty); [Benchmark] public async Task OpenFeatureClient_GetObjectValue_WithEmptyEvaluationContext_WithEmptyFlagEvaluationOptions() => - await _client.GetObjectValueAsync(_flagName, _defaultStructureValue, EvaluationContext.Empty, _emptyFlagOptions); + await this._client.GetObjectValueAsync(this._flagName, this._defaultStructureValue, EvaluationContext.Empty, this._emptyFlagOptions); } } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index a50f3945..d0870ec3 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -41,7 +41,7 @@ public EvaluationStepDefinitions(ScenarioContext scenarioContext) [Given(@"a provider is registered")] public void GivenAProviderIsRegistered() { - var memProvider = new InMemoryProvider(e2eFlagConfig); + var memProvider = new InMemoryProvider(this.e2eFlagConfig); Api.Instance.SetProviderAsync(memProvider).Wait(); client = Api.Instance.GetClient("TestClient", "1.0.0"); } @@ -204,9 +204,9 @@ public void Whencontextcontainskeyswithvalues(string field1, string field2, stri [When(@"a flag with key ""(.*)"" is evaluated with default value ""(.*)""")] public void Givenaflagwithkeyisevaluatedwithdefaultvalue(string flagKey, string defaultValue) { - contextAwareFlagKey = flagKey; - contextAwareDefaultValue = defaultValue; - contextAwareValue = client?.GetStringValueAsync(flagKey, contextAwareDefaultValue, context)?.Result; + this.contextAwareFlagKey = flagKey; + this.contextAwareDefaultValue = defaultValue; + this.contextAwareValue = client?.GetStringValueAsync(flagKey, this.contextAwareDefaultValue, this.context)?.Result; } [Then(@"the resolved string response should be ""(.*)""")] @@ -218,7 +218,7 @@ public void Thentheresolvedstringresponseshouldbe(string expected) [Then(@"the resolved flag value is ""(.*)"" when the context is empty")] public void Giventheresolvedflagvalueiswhenthecontextisempty(string expected) { - string? emptyContextValue = client?.GetStringValueAsync(contextAwareFlagKey!, contextAwareDefaultValue!, EvaluationContext.Empty).Result; + string? emptyContextValue = client?.GetStringValueAsync(this.contextAwareFlagKey!, this.contextAwareDefaultValue!, EvaluationContext.Empty).Result; Assert.Equal(expected, emptyContextValue); } @@ -239,8 +239,8 @@ public void Thenthedefaultstringvalueshouldbereturned() [Then(@"the reason should indicate an error and the error code should indicate a missing flag with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), notFoundDetails?.Reason); - Assert.Equal(errorCode, notFoundDetails?.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), this.notFoundDetails?.Reason); + Assert.Equal(errorCode, this.notFoundDetails?.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -260,8 +260,8 @@ public void Thenthedefaultintegervalueshouldbereturned() [Then(@"the reason should indicate an error and the error code should indicate a type mismatch with ""(.*)""")] public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { - Assert.Equal(Reason.Error.ToString(), typeErrorDetails?.Reason); - Assert.Equal(errorCode, typeErrorDetails?.ErrorType.GetDescription()); + Assert.Equal(Reason.Error.ToString(), this.typeErrorDetails?.Reason); + Assert.Equal(errorCode, this.typeErrorDetails?.ErrorType.GetDescription()); } private IDictionary e2eFlagConfig = new Dictionary(){ diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 1df3c976..2f778ada 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -103,8 +103,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def var defaultClient = openFeature.GetProviderMetadata(); var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); - defaultClient.Name.Should().Be(NoOpProvider.NoOpProviderName); - namedClient.Name.Should().Be(TestProvider.DefaultName); + defaultClient?.Name.Should().Be(NoOpProvider.NoOpProviderName); + namedClient?.Name.Should().Be(TestProvider.DefaultName); } [Fact] @@ -117,7 +117,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided() var defaultClient = openFeature.GetProviderMetadata(); - defaultClient.Name.Should().Be(TestProvider.DefaultName); + defaultClient?.Name.Should().Be(TestProvider.DefaultName); } [Fact] @@ -130,7 +130,7 @@ public async Task OpenFeature_Should_Assign_Provider_To_Existing_Client() await openFeature.SetProviderAsync(name, new TestProvider()); await openFeature.SetProviderAsync(name, new NoOpFeatureProvider()); - openFeature.GetProviderMetadata(name).Name.Should().Be(NoOpProvider.NoOpProviderName); + openFeature.GetProviderMetadata(name)?.Name.Should().Be(NoOpProvider.NoOpProviderName); } [Fact] @@ -187,7 +187,7 @@ public async Task OpenFeature_Should_Get_Metadata() var metadata = openFeature.GetProviderMetadata(); metadata.Should().NotBeNull(); - metadata.Name.Should().Be(NoOpProvider.NoOpProviderName); + metadata?.Name.Should().Be(NoOpProvider.NoOpProviderName); } [Theory] diff --git a/test/OpenFeature.Tests/TestImplementations.cs b/test/OpenFeature.Tests/TestImplementations.cs index a4fe51a4..7a1dff10 100644 --- a/test/OpenFeature.Tests/TestImplementations.cs +++ b/test/OpenFeature.Tests/TestImplementations.cs @@ -103,10 +103,10 @@ public override Task> ResolveStructureValueAsync(string public override async Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) { - await Task.Delay(initDelay).ConfigureAwait(false); - if (initException != null) + await Task.Delay(this.initDelay).ConfigureAwait(false); + if (this.initException != null) { - throw initException; + throw this.initException; } } From 12d48c249139b9e52a2b239ec85f60f436e83340 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:39:38 +1000 Subject: [PATCH 044/123] chore(deps): update actions/upload-artifact action to v4.3.4 (#278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://togithub.com/actions/upload-artifact) | action | patch | `v4.3.3` -> `v4.3.4` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.3.4`](https://togithub.com/actions/upload-artifact/releases/tag/v4.3.4) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.3...v4.3.4) ##### What's Changed - Update [@​actions/artifact](https://togithub.com/actions/artifact) version, bump dependencies by [@​robherley](https://togithub.com/robherley) in [https://github.com/actions/upload-artifact/pull/584](https://togithub.com/actions/upload-artifact/pull/584) **Full Changelog**: https://github.com/actions/upload-artifact/compare/v4.3.3...v4.3.4
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- .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 893834b9..98d1ade6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.4 with: name: nupkgs path: src/**/*.nupkg From 1f0468e551d18a542c158ae4c9b68c51215626f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:46:16 +1000 Subject: [PATCH 045/123] chore(deps): update xunit-dotnet monorepo (#279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [xunit](https://togithub.com/xunit/xunit) | `2.8.1` -> `2.9.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [xunit.runner.visualstudio](https://togithub.com/xunit/visualstudio.xunit) | `2.8.1` -> `2.8.2` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/xunit.runner.visualstudio/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/xunit.runner.visualstudio/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/xunit.runner.visualstudio/2.8.1/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/xunit.runner.visualstudio/2.8.1/2.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
xunit/xunit (xunit) ### [`v2.9.0`](https://togithub.com/xunit/xunit/compare/2.8.1...2.9.0) [Compare Source](https://togithub.com/xunit/xunit/compare/2.8.1...2.9.0)
xunit/visualstudio.xunit (xunit.runner.visualstudio) ### [`v2.8.2`](https://togithub.com/xunit/visualstudio.xunit/compare/2.8.1...2.8.2) [Compare Source](https://togithub.com/xunit/visualstudio.xunit/compare/2.8.1...2.8.2)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ceb087b..b443a8af 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,8 +24,8 @@ - - + + From d480da41c10da0c680e4283d76e8cac128392579 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:51:40 +1000 Subject: [PATCH 046/123] chore(deps): update dependency dotnet-sdk to v8.0.303 (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `8.0.301` -> `8.0.303` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v8.0.303`](https://togithub.com/dotnet/sdk/compare/v8.0.302...v8.0.303) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.302...v8.0.303) ### [`v8.0.302`](https://togithub.com/dotnet/sdk/compare/v8.0.301...v8.0.302) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.301...v8.0.302)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 635d63fc..9f8f3618 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.301" + "version": "8.0.303" } } From cc5f5bcec3b75ecefe74b5a4dad883e7f5bceaa5 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:59:13 +1000 Subject: [PATCH 047/123] feat: Drop net7 TFM (#284) ## This PR .net7 was EOL on May 14, 2024 https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- .github/workflows/ci.yml | 2 -- .github/workflows/code-coverage.yml | 1 - .github/workflows/e2e.yml | 1 - .github/workflows/release.yml | 1 - src/OpenFeature/OpenFeature.csproj | 2 +- test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj | 2 +- test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj | 2 +- test/OpenFeature.Tests/OpenFeature.Tests.csproj | 2 +- 8 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98d1ade6..bc57c30b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json @@ -68,7 +67,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 1f07ffc6..83d837eb 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -30,7 +30,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2cc0a84f..914d6809 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -25,7 +25,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d8aa265..b51c9bff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,6 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x 8.0.x source-url: https://nuget.pkg.github.com/open-feature/index.json diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index 9e272ba2..ed991c4e 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0;net7.0;net8.0;net462 + netstandard2.0;net6.0;net8.0;net462 OpenFeature README.md diff --git a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj index 81342e09..974dce5c 100644 --- a/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj +++ b/test/OpenFeature.Benchmarks/OpenFeature.Benchmarks.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 OpenFeature.Benchmark Exe diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index 757c4e8f..d91b338e 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 $(TargetFrameworks);net462 OpenFeature.E2ETests diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index 9ceac0dc..bfadbf9b 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 $(TargetFrameworks);net462 OpenFeature.Tests From 90d172c4c54e793764da0d7d695de44426a97ef0 Mon Sep 17 00:00:00 2001 From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:26:34 +1000 Subject: [PATCH 048/123] fix: Should map metadata when converting from ResolutionDetails to FlagEvaluationDetails (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This PR When converting the ResolutionDetails to FlagEvalutionDetails we aren't passing the ImmutableMetadata to the new object. ### Related Issues Fixes [#281](https://github.com/open-feature/dotnet-sdk/issues/281) ### Notes This PR is done on a common merge base so we can merge it into v1 as well ### Follow-up Tasks N/A ### How to test Unit test added to covert the missing test case --------- Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com> Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- .../Extension/ResolutionDetailsExtensions.cs | 2 +- .../OpenFeatureClientTests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs index 616e530a..f38356ad 100644 --- a/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs +++ b/src/OpenFeature/Extension/ResolutionDetailsExtensions.cs @@ -7,7 +7,7 @@ internal static class ResolutionDetailsExtensions public static FlagEvaluationDetails ToFlagEvaluationDetails(this ResolutionDetails details) { return new FlagEvaluationDetails(details.FlagKey, details.Value, details.ErrorType, details.Reason, - details.Variant, details.ErrorMessage); + details.Variant, details.ErrorMessage, details.FlagMetadata); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index 925de66a..d1a91c1f 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -11,6 +12,7 @@ using NSubstitute.ExceptionExtensions; using OpenFeature.Constant; using OpenFeature.Error; +using OpenFeature.Extension; using OpenFeature.Model; using OpenFeature.Tests.Internal; using Xunit; @@ -480,5 +482,27 @@ public void Should_Get_And_Set_Context() client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } + + + [Fact] + public void ToFlagEvaluationDetails_Should_Convert_All_Properties() + { + var fixture = new Fixture(); + var flagName = fixture.Create(); + var boolValue = fixture.Create(); + var errorType = fixture.Create(); + var reason = fixture.Create(); + var variant = fixture.Create(); + var errorMessage = fixture.Create(); + var flagData = fixture + .CreateMany>(10) + .ToDictionary(x => x.Key, x => x.Value); + var flagMetadata = new ImmutableMetadata(flagData); + + var expected = new ResolutionDetails(flagName, boolValue, errorType, reason, variant, errorMessage, flagMetadata); + var result = expected.ToFlagEvaluationDetails(); + + result.Should().BeEquivalentTo(expected); + } } } From 2072e45675039daaab4921392d9af302ac4f0ce6 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 29 Jul 2024 13:53:15 -0400 Subject: [PATCH 049/123] feat: back targetingKey with internal map (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the internal dictionary for the `targetingKey`. This is non-breaking from a compiler perspective. It could result in some behavioral changes, but IMO they are largely desirable. Fixes: https://github.com/open-feature/dotnet-sdk/issues/235 --------- Signed-off-by: Todd Baert Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- src/OpenFeature/Model/EvaluationContext.cs | 23 +++++--- .../Model/EvaluationContextBuilder.cs | 23 +------- .../OpenFeatureEvaluationContextTests.cs | 52 ++++++++++++++++++- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/OpenFeature/Model/EvaluationContext.cs b/src/OpenFeature/Model/EvaluationContext.cs index 59b1fe20..304e4cd9 100644 --- a/src/OpenFeature/Model/EvaluationContext.cs +++ b/src/OpenFeature/Model/EvaluationContext.cs @@ -11,26 +11,30 @@ namespace OpenFeature.Model /// Evaluation context public sealed class EvaluationContext { + /// + /// The index for the "targeting key" property when the EvaluationContext is serialized or expressed as a dictionary. + /// + internal const string TargetingKeyIndex = "targetingKey"; + + private readonly Structure _structure; /// /// Internal constructor used by the builder. /// - /// The targeting key - /// The content of the context. - internal EvaluationContext(string? targetingKey, Structure content) + /// + internal EvaluationContext(Structure content) { - this.TargetingKey = targetingKey; this._structure = content; } + /// /// Private constructor for making an empty . /// private EvaluationContext() { this._structure = Structure.Empty; - this.TargetingKey = string.Empty; } /// @@ -89,7 +93,14 @@ public IImmutableDictionary AsDictionary() /// /// Returns the targeting key for the context. /// - public string? TargetingKey { get; } + public string? TargetingKey + { + get + { + this._structure.TryGetValue(TargetingKeyIndex, out Value? targetingKey); + return targetingKey?.AsString; + } + } /// /// Return an enumerator for all values diff --git a/src/OpenFeature/Model/EvaluationContextBuilder.cs b/src/OpenFeature/Model/EvaluationContextBuilder.cs index c672c401..30e2ffe0 100644 --- a/src/OpenFeature/Model/EvaluationContextBuilder.cs +++ b/src/OpenFeature/Model/EvaluationContextBuilder.cs @@ -14,8 +14,6 @@ public sealed class EvaluationContextBuilder { private readonly StructureBuilder _attributes = Structure.Builder(); - internal string? TargetingKey { get; private set; } - /// /// Internal to only allow direct creation by . /// @@ -28,7 +26,7 @@ internal EvaluationContextBuilder() { } /// This builder public EvaluationContextBuilder SetTargetingKey(string targetingKey) { - this.TargetingKey = targetingKey; + this._attributes.Set(EvaluationContext.TargetingKeyIndex, targetingKey); return this; } @@ -138,23 +136,6 @@ public EvaluationContextBuilder Set(string key, DateTime value) /// This builder public EvaluationContextBuilder Merge(EvaluationContext context) { - string? newTargetingKey = ""; - - if (!string.IsNullOrWhiteSpace(this.TargetingKey)) - { - newTargetingKey = this.TargetingKey; - } - - if (!string.IsNullOrWhiteSpace(context.TargetingKey)) - { - newTargetingKey = context.TargetingKey; - } - - if (!string.IsNullOrWhiteSpace(newTargetingKey)) - { - this.TargetingKey = newTargetingKey; - } - foreach (var kvp in context) { this.Set(kvp.Key, kvp.Value); @@ -169,7 +150,7 @@ public EvaluationContextBuilder Merge(EvaluationContext context) /// An immutable public EvaluationContext Build() { - return new EvaluationContext(this.TargetingKey, this._attributes.Build()); + return new EvaluationContext(this._attributes.Build()); } } } diff --git a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs index 5329620f..826ac68e 100644 --- a/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEvaluationContextTests.cs @@ -160,7 +160,7 @@ public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() var key = "testKey"; var expectedValue = new Value("testValue"); var structure = new Structure(new Dictionary { { key, expectedValue } }); - var evaluationContext = new EvaluationContext("targetingKey", structure); + var evaluationContext = new EvaluationContext(structure); // Act var result = evaluationContext.TryGetValue(key, out var actualValue); @@ -169,5 +169,55 @@ public void TryGetValue_WhenCalledWithExistingKey_ReturnsTrueAndExpectedValue() Assert.True(result); Assert.Equal(expectedValue, actualValue); } + + [Fact] + public void GetValueOnTargetingKeySetWithTargetingKey_Equals_TargetingKey() + { + // Arrange + var value = "my_targeting_key"; + var evaluationContext = EvaluationContext.Builder().SetTargetingKey(value).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Equal(value, actualFromStructure?.AsString); + Assert.Equal(value, actualFromTargetingKey); + } + + [Fact] + public void GetValueOnTargetingKeySetWithStructure_Equals_TargetingKey() + { + // Arrange + var value = "my_targeting_key"; + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(value)).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Equal(value, actualFromStructure?.AsString); + Assert.Equal(value, actualFromTargetingKey); + } + + [Fact] + public void GetValueOnTargetingKeySetWithNonStringValue_Equals_Null() + { + // Arrange + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContext.TargetingKeyIndex, new Value(1)).Build(); + + // Act + var result = evaluationContext.TryGetValue(EvaluationContext.TargetingKeyIndex, out var actualFromStructure); + var actualFromTargetingKey = evaluationContext.TargetingKey; + + // Assert + Assert.True(result); + Assert.Null(actualFromStructure?.AsString); + Assert.Null(actualFromTargetingKey); + } } } From c503b304810e9420f4cb902bd14a3924e40b6256 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:20:59 +1000 Subject: [PATCH 050/123] chore(deps): update actions/upload-artifact action to v4.3.5 (#291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://togithub.com/actions/upload-artifact) | action | patch | `v4.3.4` -> `v4.3.5` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v4.3.5`](https://togithub.com/actions/upload-artifact/compare/v4.3.4...v4.3.5) [Compare Source](https://togithub.com/actions/upload-artifact/compare/v4.3.4...v4.3.5)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- .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 bc57c30b..13092d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: nupkgs path: src/**/*.nupkg From 9a5a343cd78830ee088cd6df692914396326be17 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 13 Aug 2024 14:16:16 -0400 Subject: [PATCH 051/123] feat!: domain instead of client name (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses "domain" terminology instead of "client name / named client". Fixes: https://github.com/open-feature/dotnet-sdk/issues/249 I believe with this, we are able to release a 2.0 --------- Signed-off-by: Todd Baert Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- README.md | 38 ++++++------ src/OpenFeature/Api.cs | 32 +++++----- src/OpenFeature/ProviderRepository.cs | 22 +++---- .../OpenFeatureClientBenchmarks.cs | 6 +- .../OpenFeatureClientTests.cs | 60 +++++++++---------- .../OpenFeatureEventTests.cs | 8 +-- .../OpenFeature.Tests/OpenFeatureHookTests.cs | 4 +- test/OpenFeature.Tests/OpenFeatureTests.cs | 4 +- 8 files changed, 89 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 6844915f..bce047f8 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,16 @@ public async Task Example() ## 🌟 Features -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| ✅ | [Logging](#logging) | Integrate with popular logging packages. | -| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| Status | Features | Description | +| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Domains](#domains) | Logically bind clients with providers. | +| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | > Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ @@ -96,7 +96,7 @@ await Api.Instance.SetProviderAsync(new MyProvider()); ``` In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more detail below. +This is possible using [domains](#domains), which is covered in more detail below. ### Targeting @@ -151,27 +151,29 @@ var value = await client.GetBooleanValueAsync("boolFlag", false, context, new Fl The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. -### Named clients +### Domains -Clients can be given a name. -A name is a logical identifier that can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. +Clients can be assigned to a domain. +A domain is a logical identifier which can be used to associate clients with a particular provider. +If a domain has no associated provider, the default provider is used. ```csharp // registering the default provider await Api.Instance.SetProviderAsync(new LocalProvider()); -// registering a named provider +// registering a provider to a domain await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); // a client backed by default provider FeatureClient clientDefault = Api.Instance.GetClient(); // a client backed by CachedProvider -FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); - +FeatureClient scopedClient = Api.Instance.GetClient("clientForCache"); ``` +Domains can be defined on a provider during registration. +For more details, please refer to the [providers](#providers) section. + ### Eventing Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 3fa38916..fae9916b 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -50,19 +50,19 @@ public async Task SetProviderAsync(FeatureProvider featureProvider) } /// - /// Sets the feature provider to given clientName. In order to wait for the provider to be set, and + /// Binds the feature provider to the given domain. In order to wait for the provider to be set, and /// initialization to complete, await the returned task. /// - /// Name of client + /// An identifier which logically binds clients with providers /// Implementation of - public async Task SetProviderAsync(string clientName, FeatureProvider featureProvider) + public async Task SetProviderAsync(string domain, FeatureProvider featureProvider) { - if (string.IsNullOrWhiteSpace(clientName)) + if (string.IsNullOrWhiteSpace(domain)) { - throw new ArgumentNullException(nameof(clientName)); + throw new ArgumentNullException(nameof(domain)); } - this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider); - await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); + this._eventExecutor.RegisterClientFeatureProvider(domain, featureProvider); + await this._repository.SetProviderAsync(domain, featureProvider, this.GetContext(), this.AfterInitialization, this.AfterError).ConfigureAwait(false); } /// @@ -82,14 +82,15 @@ public FeatureProvider GetProvider() } /// - /// Gets the feature provider with given clientName + /// Gets the feature provider with given domain /// - /// Name of client - /// A provider associated with the given clientName, if clientName is empty or doesn't + /// An identifier which logically binds clients with providers + + /// A provider associated with the given domain, if domain is empty or doesn't /// have a corresponding provider the default provider will be returned - public FeatureProvider GetProvider(string clientName) + public FeatureProvider GetProvider(string domain) { - return this._repository.GetProvider(clientName); + return this._repository.GetProvider(domain); } /// @@ -104,12 +105,13 @@ public FeatureProvider GetProvider(string clientName) public Metadata? GetProviderMetadata() => this.GetProvider().GetMetadata(); /// - /// Gets providers metadata assigned to the given clientName. If the clientName has no provider + /// Gets providers metadata assigned to the given domain. If the domain has no provider /// assigned to it the default provider will be returned /// - /// Name of client + /// An identifier which logically binds clients with providers + /// Metadata assigned to provider - public Metadata? GetProviderMetadata(string clientName) => this.GetProvider(clientName).GetMetadata(); + public Metadata? GetProviderMetadata(string domain) => this.GetProvider(domain).GetMetadata(); /// /// Create a new instance of using the current provider diff --git a/src/OpenFeature/ProviderRepository.cs b/src/OpenFeature/ProviderRepository.cs index 760503b6..49f1de43 100644 --- a/src/OpenFeature/ProviderRepository.cs +++ b/src/OpenFeature/ProviderRepository.cs @@ -127,7 +127,7 @@ private static async Task InitProviderAsync( /// /// Set a named provider /// - /// the name to associate with the provider + /// an identifier which logically binds clients with providers /// the provider to set as the default, passing null has no effect /// the context to initialize the provider with /// @@ -138,15 +138,15 @@ private static async Task InitProviderAsync( /// initialization /// /// The to cancel any async side effects. - public async Task SetProviderAsync(string? clientName, + public async Task SetProviderAsync(string? domain, FeatureProvider? featureProvider, EvaluationContext context, Func? afterInitSuccess = null, Func? afterInitError = null, CancellationToken cancellationToken = default) { - // Cannot set a provider for a null clientName. - if (clientName == null) + // Cannot set a provider for a null domain. + if (domain == null) { return; } @@ -155,17 +155,17 @@ public async Task SetProviderAsync(string? clientName, try { - this._featureProviders.TryGetValue(clientName, out var oldProvider); + this._featureProviders.TryGetValue(domain, out var oldProvider); if (featureProvider != null) { - this._featureProviders.AddOrUpdate(clientName, featureProvider, + this._featureProviders.AddOrUpdate(domain, featureProvider, (key, current) => featureProvider); } else { // If names of clients are programmatic, then setting the provider to null could result // in unbounded growth of the collection. - this._featureProviders.TryRemove(clientName, out _); + this._featureProviders.TryRemove(domain, out _); } // We want to allow shutdown to happen concurrently with initialization, and the caller to not @@ -238,22 +238,22 @@ public FeatureProvider GetProvider() } } - public FeatureProvider GetProvider(string? clientName) + public FeatureProvider GetProvider(string? domain) { #if NET6_0_OR_GREATER - if (string.IsNullOrEmpty(clientName)) + if (string.IsNullOrEmpty(domain)) { return this.GetProvider(); } #else // This is a workaround for the issue in .NET Framework where string.IsNullOrEmpty is not nullable compatible. - if (clientName == null || string.IsNullOrEmpty(clientName)) + if (domain == null || string.IsNullOrEmpty(domain)) { return this.GetProvider(); } #endif - return this._featureProviders.TryGetValue(clientName, out var featureProvider) + return this._featureProviders.TryGetValue(domain, out var featureProvider) ? featureProvider : this.GetProvider(); } diff --git a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs index 3796821e..03650144 100644 --- a/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs +++ b/test/OpenFeature.Benchmarks/OpenFeatureClientBenchmarks.cs @@ -16,7 +16,7 @@ namespace OpenFeature.Benchmark [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")] public class OpenFeatureClientBenchmarks { - private readonly string _clientName; + private readonly string _domain; private readonly string _clientVersion; private readonly string _flagName; private readonly bool _defaultBoolValue; @@ -30,7 +30,7 @@ public class OpenFeatureClientBenchmarks public OpenFeatureClientBenchmarks() { var fixture = new Fixture(); - this._clientName = fixture.Create(); + this._domain = fixture.Create(); this._clientVersion = fixture.Create(); this._flagName = fixture.Create(); this._defaultBoolValue = fixture.Create(); @@ -40,7 +40,7 @@ public OpenFeatureClientBenchmarks() this._defaultStructureValue = fixture.Create(); this._emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); - this._client = Api.Instance.GetClient(this._clientName, this._clientVersion); + this._client = Api.Instance.GetClient(this._domain, this._clientVersion); } [Benchmark] diff --git a/test/OpenFeature.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.Tests/OpenFeatureClientTests.cs index d1a91c1f..ce3e9e93 100644 --- a/test/OpenFeature.Tests/OpenFeatureClientTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureClientTests.cs @@ -27,13 +27,13 @@ public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture public void OpenFeatureClient_Should_Allow_Hooks() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var hook1 = Substitute.For(); var hook2 = Substitute.For(); var hook3 = Substitute.For(); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); client.AddHooks(new[] { hook1, hook2 }); @@ -53,11 +53,11 @@ public void OpenFeatureClient_Should_Allow_Hooks() public void OpenFeatureClient_Metadata_Should_Have_Name() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); - client.GetMetadata().Name.Should().Be(clientName); + client.GetMetadata().Name.Should().Be(domain); client.GetMetadata().Version.Should().Be(clientVersion); } @@ -68,7 +68,7 @@ public void OpenFeatureClient_Metadata_Should_Have_Name() public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); @@ -79,7 +79,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetBooleanValueAsync(flagName, defaultBoolValue)).Should().Be(defaultBoolValue); (await client.GetBooleanValueAsync(flagName, defaultBoolValue, EvaluationContext.Empty)).Should().Be(defaultBoolValue); @@ -114,7 +114,7 @@ public async Task OpenFeatureClient_Should_Allow_Flag_Evaluation() public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultBoolValue = fixture.Create(); @@ -125,7 +125,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() var emptyFlagOptions = new FlagEvaluationOptions(ImmutableList.Empty, ImmutableDictionary.Empty); await Api.Instance.SetProviderAsync(new NoOpFeatureProvider()); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); var boolFlagEvaluationDetails = new FlagEvaluationDetails(flagName, defaultBoolValue, ErrorType.None, NoOpProvider.ReasonNoOp, NoOpProvider.Variant); (await client.GetBooleanDetailsAsync(flagName, defaultBoolValue)).Should().BeEquivalentTo(boolFlagEvaluationDetails); @@ -163,7 +163,7 @@ public async Task OpenFeatureClient_Should_Allow_Details_Flag_Evaluation() public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatch() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -176,7 +176,7 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc mockedFeatureProvider.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(mockedFeatureProvider); - var client = Api.Instance.GetClient(clientName, clientVersion, mockedLogger); + var client = Api.Instance.GetClient(domain, clientVersion, mockedLogger); var evaluationDetails = await client.GetObjectDetailsAsync(flagName, defaultValue); evaluationDetails.ErrorType.Should().Be(ErrorType.TypeMismatch); @@ -277,7 +277,7 @@ public async Task Must_Short_Circuit_Fatal() public async Task Should_Resolve_BooleanValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -288,7 +288,7 @@ public async Task Should_Resolve_BooleanValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetBooleanValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -299,7 +299,7 @@ public async Task Should_Resolve_BooleanValue() public async Task Should_Resolve_StringValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -310,7 +310,7 @@ public async Task Should_Resolve_StringValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetStringValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -321,7 +321,7 @@ public async Task Should_Resolve_StringValue() public async Task Should_Resolve_IntegerValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -332,7 +332,7 @@ public async Task Should_Resolve_IntegerValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetIntegerValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -343,7 +343,7 @@ public async Task Should_Resolve_IntegerValue() public async Task Should_Resolve_DoubleValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -354,7 +354,7 @@ public async Task Should_Resolve_DoubleValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetDoubleValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -365,7 +365,7 @@ public async Task Should_Resolve_DoubleValue() public async Task Should_Resolve_StructureValue() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -376,7 +376,7 @@ public async Task Should_Resolve_StructureValue() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); (await client.GetObjectValueAsync(flagName, defaultValue)).Should().Be(defaultValue); @@ -387,7 +387,7 @@ public async Task Should_Resolve_StructureValue() public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -399,7 +399,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); @@ -412,7 +412,7 @@ public async Task When_Error_Is_Returned_From_Provider_Should_Return_Error() public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -424,7 +424,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); await Api.Instance.SetProviderAsync(featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); var response = await client.GetObjectDetailsAsync(flagName, defaultValue); response.ErrorType.Should().Be(ErrorType.ParseError); @@ -437,7 +437,7 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error() public async Task Cancellation_Token_Added_Is_Passed_To_Provider() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultString = fixture.Create(); @@ -459,8 +459,8 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create())); featureProviderMock.GetProviderHooks().Returns(ImmutableList.Empty); - await Api.Instance.SetProviderAsync(clientName, featureProviderMock); - var client = Api.Instance.GetClient(clientName, clientVersion); + await Api.Instance.SetProviderAsync(domain, featureProviderMock); + var client = Api.Instance.GetClient(domain, clientVersion); var task = client.GetStringDetailsAsync(flagName, defaultString, EvaluationContext.Empty, null, cts.Token); cts.Cancel(); // cancel before awaiting @@ -474,11 +474,11 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider() public void Should_Get_And_Set_Context() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var KEY = "key"; var VAL = 1; - FeatureClient client = Api.Instance.GetClient(clientName, clientVersion); + FeatureClient client = Api.Instance.GetClient(domain, clientVersion); client.SetContext(new EvaluationContextBuilder().Set(KEY, VAL).Build()); Assert.Equal(VAL, client.GetContext().GetValue(KEY).AsInteger); } diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index a7bcd2e7..a4b0d111 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -304,9 +304,9 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() var fixture = new Fixture(); var eventHandler = Substitute.For(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); - var myClient = Api.Instance.GetClient(clientName, clientVersion); + var myClient = Api.Instance.GetClient(domain, clientVersion); var testProvider = new TestProvider(); await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name!, testProvider); @@ -332,9 +332,9 @@ public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Hand failingEventHandler.When(x => x.Invoke(Arg.Any())) .Do(x => throw new Exception()); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); - var myClient = Api.Instance.GetClient(clientName, clientVersion); + var myClient = Api.Instance.GetClient(domain, clientVersion); myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); diff --git a/test/OpenFeature.Tests/OpenFeatureHookTests.cs b/test/OpenFeature.Tests/OpenFeatureHookTests.cs index 9ca5b364..cc8b08a1 100644 --- a/test/OpenFeature.Tests/OpenFeatureHookTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureHookTests.cs @@ -27,7 +27,7 @@ public class OpenFeatureHookTests : ClearOpenFeatureInstanceFixture public async Task Hooks_Should_Be_Called_In_Order() { var fixture = new Fixture(); - var clientName = fixture.Create(); + var domain = fixture.Create(); var clientVersion = fixture.Create(); var flagName = fixture.Create(); var defaultValue = fixture.Create(); @@ -54,7 +54,7 @@ public async Task Hooks_Should_Be_Called_In_Order() testProvider.AddHook(providerHook); Api.Instance.AddHooks(apiHook); await Api.Instance.SetProviderAsync(testProvider); - var client = Api.Instance.GetClient(clientName, clientVersion); + var client = Api.Instance.GetClient(domain, clientVersion); client.AddHooks(clientHook); await client.GetBooleanValueAsync(flagName, defaultValue, EvaluationContext.Empty, diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs index 2f778ada..acc53b61 100644 --- a/test/OpenFeature.Tests/OpenFeatureTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureTests.cs @@ -101,10 +101,10 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()); var defaultClient = openFeature.GetProviderMetadata(); - var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); + var domainScopedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName); defaultClient?.Name.Should().Be(NoOpProvider.NoOpProviderName); - namedClient?.Name.Should().Be(TestProvider.DefaultName); + domainScopedClient?.Name.Should().Be(TestProvider.DefaultName); } [Fact] From be43fcc9313bda1f75f0bbeb9b9bd65770a1f417 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:01:43 -0400 Subject: [PATCH 052/123] chore(deps): update dependency benchmarkdotnet to v0.14.0 (#293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [BenchmarkDotNet](https://togithub.com/dotnet/BenchmarkDotNet) | `0.13.1` -> `0.14.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/BenchmarkDotNet/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/BenchmarkDotNet/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/BenchmarkDotNet/0.13.1/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/BenchmarkDotNet/0.13.1/0.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
dotnet/BenchmarkDotNet (BenchmarkDotNet) ### [`v0.14.0`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.14.0): 0.14.0 Full changelog: https://benchmarkdotnet.org/changelog/v0.14.0.html #### Highlights - Introduce `BenchmarkDotNet.Diagnostics.dotMemory` [#​2549](https://togithub.com/dotnet/BenchmarkDotNet/pull/2549): memory allocation profile of your benchmarks using [dotMemory](https://www.jetbrains.com/dotmemory/), see [@​BenchmarkDotNet](https://togithub.com/BenchmarkDotNet).Samples.IntroDotMemoryDiagnoser - Introduce `BenchmarkDotNet.Exporters.Plotting` [#​2560](https://togithub.com/dotnet/BenchmarkDotNet/pull/2560): plotting via [ScottPlot](https://scottplot.net/) (initial version) - Multiple bugfixes - The default build toolchains have been updated to pass `IntermediateOutputPath`, `OutputPath`, and `OutDir` properties to the `dotnet build` command. This change forces all build outputs to be placed in a new directory generated by BenchmarkDotNet, and fixes many issues that have been reported with builds. You can also access these paths in your own `.csproj` and `.props` from those properties if you need to copy custom files to the output. #### Bug fixes - Fixed multiple build-related bugs including passing MsBuildArguments and .Net 8's `UseArtifactsOutput`. #### Breaking Changes - `DotNetCliBuilder` removed `retryFailedBuildWithNoDeps` constructor option. - `DotNetCliCommand` removed `RetryFailedBuildWithNoDeps` property and `BuildNoRestoreNoDependencies()` and `PublishNoBuildAndNoRestore()` methods (replaced with `PublishNoRestore()`). ### [`v0.13.12`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.12): 0.13.12 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.12.html #### Highlights The biggest highlight of this release if our new VSTest Adapter, which allows to run benchmarks as unit tests in your favorite IDE! The detailed guide can be found [here](https://benchmarkdotnet.org/articles/features/vstest.html). This release also includes to a minor bug fix that caused incorrect job id generation: fixed job id generation ([#​2491](https://togithub.com/dotnet/BenchmarkDotNet/pull/2491)). Also, the target framework in the BenchmarkDotNet templates was bumped to .NET 8.0. ### [`v0.13.11`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.11): 0.13.11 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.11.html In the [v0.13.11](https://togithub.com/dotnet/BenchmarkDotNet/issues?q=milestone:v0.13.11) scope, 4 issues were resolved and 8 pull requests were merged. This release includes 28 commits by 7 contributors. #### Resolved issues (4) - [#​2060](https://togithub.com/dotnet/BenchmarkDotNet/issues/2060) NativeAOT benchmark started from .Net Framework host doesn't have all intrinsics enabled (assignee: [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2233](https://togithub.com/dotnet/BenchmarkDotNet/issues/2233) Q: Include hardware counters in XML output (assignee: [@​nazulg](https://togithub.com/nazulg)) - [#​2388](https://togithub.com/dotnet/BenchmarkDotNet/issues/2388) Include AVX512 in listed HardwareIntrinsics - [#​2463](https://togithub.com/dotnet/BenchmarkDotNet/issues/2463) Bug. Native AOT .NET 7.0 doesn't work. System.NotSupportedException: X86Serialize (assignee: [@​adamsitnik](https://togithub.com/adamsitnik)) #### Merged pull requests (8) - [#​2412](https://togithub.com/dotnet/BenchmarkDotNet/pull/2412) Add HardwareIntrinsics AVX-512 info (by [@​nietras](https://togithub.com/nietras)) - [#​2458](https://togithub.com/dotnet/BenchmarkDotNet/pull/2458) Adds Metrics Columns to Benchmark Report Output (by [@​nazulg](https://togithub.com/nazulg)) - [#​2459](https://togithub.com/dotnet/BenchmarkDotNet/pull/2459) Enable MemoryDiagnoser on Legacy Mono (by [@​MichalPetryka](https://togithub.com/MichalPetryka)) - [#​2462](https://togithub.com/dotnet/BenchmarkDotNet/pull/2462) update SDK to .NET 8 (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2464](https://togithub.com/dotnet/BenchmarkDotNet/pull/2464) Use "native" for .NET 8, don't use "serialize" for .NET 7 (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2465](https://togithub.com/dotnet/BenchmarkDotNet/pull/2465) fix NativeAOT toolchain and tests (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [#​2468](https://togithub.com/dotnet/BenchmarkDotNet/pull/2468) Add OperationsPerSecondAttribute (by [@​DarkWanderer](https://togithub.com/DarkWanderer)) - [#​2475](https://togithub.com/dotnet/BenchmarkDotNet/pull/2475) Fix some tests (by [@​timcassell](https://togithub.com/timcassell)) #### Commits (28) - [bb55e6](https://togithub.com/dotnet/BenchmarkDotNet/commit/bb55e6b067829c74e04838255e96d949857d5731) Set next BenchmarkDotNet version: 0.13.11 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [db4d8b](https://togithub.com/dotnet/BenchmarkDotNet/commit/db4d8b6d8a652db4bb1e4b1b4b0cd9df917e9584) Adds Metrics Columns to Benchmark Report Output ([#​2458](https://togithub.com/dotnet/BenchmarkDotNet/issues/2458)) (by [@​nazulg](https://togithub.com/nazulg)) - [e93b2b](https://togithub.com/dotnet/BenchmarkDotNet/commit/e93b2b1b332fc90da4934025e2edba7d67a15b54) Use "native" for .NET 8, don't use "serialize" for .NET 7 ([#​2464](https://togithub.com/dotnet/BenchmarkDotNet/issues/2464)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [127157](https://togithub.com/dotnet/BenchmarkDotNet/commit/127157924014afe2d0b58398d682381a855d7c34) \[build] Fix spellcheck-docs workflow (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8a02ec](https://togithub.com/dotnet/BenchmarkDotNet/commit/8a02ec28d55529f9be0ea66d843049738b2be8fa) \[build] Use our .NET SDK on Windows (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [1b39e8](https://togithub.com/dotnet/BenchmarkDotNet/commit/1b39e8e6d5437bdbf0bb62986e680e54b19cc873) Suppress NU1903 in IntegrationTests.ManualRunning.MultipleFrameworks (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [e90311](https://togithub.com/dotnet/BenchmarkDotNet/commit/e90311539d78e4bf9d90c6aeae9f40219b31a4ac) update SDK to .NET 8 ([#​2462](https://togithub.com/dotnet/BenchmarkDotNet/issues/2462)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [fc7afe](https://togithub.com/dotnet/BenchmarkDotNet/commit/fc7afeddcff7a52ccee165ac99ba216e8eb138ab) Enable MemoryDiagnoser on Legacy Mono ([#​2459](https://togithub.com/dotnet/BenchmarkDotNet/issues/2459)) (by [@​MichalPetryka](https://togithub.com/MichalPetryka)) - [630622](https://togithub.com/dotnet/BenchmarkDotNet/commit/630622b6df3192f766ffa03ff07b5086e70cb264) fix NativeAOT toolchain and tests ([#​2465](https://togithub.com/dotnet/BenchmarkDotNet/issues/2465)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [536a28](https://togithub.com/dotnet/BenchmarkDotNet/commit/536a28e0ff2196255fb120aa0d39e40bdbde454a) Add HardwareIntrinsics AVX-512 info ([#​2412](https://togithub.com/dotnet/BenchmarkDotNet/issues/2412)) (by [@​nietras](https://togithub.com/nietras)) - [3fa045](https://togithub.com/dotnet/BenchmarkDotNet/commit/3fa0456495cac82b536902b101a2972c62c3e4a8) Add OperationsPerSecondAttribute ([#​2468](https://togithub.com/dotnet/BenchmarkDotNet/issues/2468)) (by [@​DarkWanderer](https://togithub.com/DarkWanderer)) - [0583cb](https://togithub.com/dotnet/BenchmarkDotNet/commit/0583cb90739b3ee4b8258f807ef42cdc3243f82f) Bump Microsoft.NETCore.Platforms: 5.0.0->6.0.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [2e62b9](https://togithub.com/dotnet/BenchmarkDotNet/commit/2e62b9b0a8c80255914e9e11d06d92871df40f85) Remove netcoreapp2.0;net461 from TFMs for IntegrationTests.ManualRunning.Mult... (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [92fa3f](https://togithub.com/dotnet/BenchmarkDotNet/commit/92fa3f834e0519d32fd8fc97e26aa82f9626b241) Bump xunit: 2.5.0->2.6.2 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [01e220](https://togithub.com/dotnet/BenchmarkDotNet/commit/01e2201c826dd44e089a22c40d8c3abecba320fa) Bump xunit.runner.visualstudio: 2.5.0->2.5.4 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [29a94c](https://togithub.com/dotnet/BenchmarkDotNet/commit/29a94ce301dac6121d1e0d1a0d783a6491c27703) Bump Verify.Xunit: 20.3.2->20.8.2 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [538e0e](https://togithub.com/dotnet/BenchmarkDotNet/commit/538e0e1771be037ef587b08cb52515ce6daf5c0e) Bump Microsoft.NET.Test.SDK: 17.6.2->17.8.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [136e4b](https://togithub.com/dotnet/BenchmarkDotNet/commit/136e4bb3f18a419df40c18a5430a29243ab57fd8) Remove explicit Microsoft.NETFramework.ReferenceAssemblies reference in Bench... (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [423b84](https://togithub.com/dotnet/BenchmarkDotNet/commit/423b8473d02d5bd59617675190660222198bf7d0) \[build] Bump Docfx.App: 2.71.1->2.74.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [718953](https://togithub.com/dotnet/BenchmarkDotNet/commit/718953674a83da4de6563368f38776048024f0d3) \[build] Bump Octokit: 7.0.0->9.0.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [0cce91](https://togithub.com/dotnet/BenchmarkDotNet/commit/0cce9120bd717e31a4a6a4a396faa8f38fd3cc08) \[build] Bump Cake.Frosting: 3.2.0->4.0.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [4d5dc9](https://togithub.com/dotnet/BenchmarkDotNet/commit/4d5dc9ca13072d384cabf565bc3622f8de5626d7) Fix Newtonsoft.Json v13.0.1 in BenchmarkDotNet.IntegrationTests (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [c7ec60](https://togithub.com/dotnet/BenchmarkDotNet/commit/c7ec60ad6d4e54a99463eb46a0307196cc040940) Enable UserCanSpecifyCustomNuGetPackageDependency test on Linux (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [a572db](https://togithub.com/dotnet/BenchmarkDotNet/commit/a572db119798fb58b24437ccef6a364efd59e836) Bump C# LangVersion: 11.0->12.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [b4ac9d](https://togithub.com/dotnet/BenchmarkDotNet/commit/b4ac9df9f7890ca9669e2b9c8835af35c072a453) Nullability cleanup (2023-11-26) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [5557ae](https://togithub.com/dotnet/BenchmarkDotNet/commit/5557aee0638bda38001bd6c2000164d9b96d315a) \[build] Bump Docfx.App: 2.74.0->2.74.1 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [b987b9](https://togithub.com/dotnet/BenchmarkDotNet/commit/b987b99ed37455e5443ed03169890998c3152ae9) Fixed some tests. ([#​2475](https://togithub.com/dotnet/BenchmarkDotNet/issues/2475)) (by [@​timcassell](https://togithub.com/timcassell)) - [05eb00](https://togithub.com/dotnet/BenchmarkDotNet/commit/05eb00f3536061ca624bab3d9a4ca2f3c0be5922) Prepare v0.13.11 changelog (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Contributors (7) - Adam Sitnik ([@​adamsitnik](https://togithub.com/adamsitnik)) - Andrey Akinshin ([@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - Michał Petryka ([@​MichalPetryka](https://togithub.com/MichalPetryka)) - Nazul Grimaldo ([@​nazulg](https://togithub.com/nazulg)) - nietras ([@​nietras](https://togithub.com/nietras)) - Oleg V. Kozlyuk ([@​DarkWanderer](https://togithub.com/DarkWanderer)) - Tim Cassell ([@​timcassell](https://togithub.com/timcassell)) Thank you very much! ### [`v0.13.10`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.10): 0.13.10 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.10.html #### Highlights Initial support of .NET 9 and minor bug fixes. #### Details In the [v0.13.10](https://togithub.com/dotnet/BenchmarkDotNet/issues?q=milestone:v0.13.10) scope, 2 issues were resolved and 3 pull requests were merged. This release includes 10 commits by 4 contributors. #### Resolved issues (2) - [#​2436](https://togithub.com/dotnet/BenchmarkDotNet/issues/2436) BenchmarkDotNet Access Denied Error on WSL2 when Writing to '/mnt/c/DumpStack.log.tmp' (assignee: [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [#​2455](https://togithub.com/dotnet/BenchmarkDotNet/issues/2455) .NET 9 support (assignee: [@​adamsitnik](https://togithub.com/adamsitnik)) #### Merged pull requests (3) - [#​2447](https://togithub.com/dotnet/BenchmarkDotNet/pull/2447) Add support for wasm/net9.0 (by [@​radical](https://togithub.com/radical)) - [#​2453](https://togithub.com/dotnet/BenchmarkDotNet/pull/2453) feat: set RuntimeHostConfigurationOption on generated project (by [@​workgroupengineering](https://togithub.com/workgroupengineering)) - [#​2456](https://togithub.com/dotnet/BenchmarkDotNet/pull/2456) implement full .NET 9 support (by [@​adamsitnik](https://togithub.com/adamsitnik)) #### Commits (10) - [c27152](https://togithub.com/dotnet/BenchmarkDotNet/commit/c27152b9d7b6391501abcf7e8edcb2804999622f) Set next BenchmarkDotNet version: 0.13.10 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [2e96d2](https://togithub.com/dotnet/BenchmarkDotNet/commit/2e96d29453a804cfc1b92fffeea94c866522167a) Don't show AssemblyInformationalVersion metadata in BDN BrandVersion (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [d17c6a](https://togithub.com/dotnet/BenchmarkDotNet/commit/d17c6ad0bd8ac15d83ced0a7522de7dd51526ad4) Support Windows 11 23H2 (10.0.22631) in OsBrandStringHelper (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [af9c5c](https://togithub.com/dotnet/BenchmarkDotNet/commit/af9c5c6013b4e661cda0ff8fed40a50ae62d5a74) Exception handling in DotNetCliGenerator.GetRootDirectory, fix [#​2436](https://togithub.com/dotnet/BenchmarkDotNet/issues/2436) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [e11136](https://togithub.com/dotnet/BenchmarkDotNet/commit/e11136897bdf26c004076bcbe812bb4ae60f8859) \[build] Bump Docfx.App: 2.71.0->2.71.1 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [7b342f](https://togithub.com/dotnet/BenchmarkDotNet/commit/7b342f5cfb63c73708f3e69dde33d7430a3c0401) Add support for wasm/net9.0 ([#​2447](https://togithub.com/dotnet/BenchmarkDotNet/issues/2447)) (by [@​radical](https://togithub.com/radical)) - [e17068](https://togithub.com/dotnet/BenchmarkDotNet/commit/e170684208103ca5ba4212ad8dc7c2aad5cf02d4) Adjust 'Failed to set up high priority' message (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [0a734a](https://togithub.com/dotnet/BenchmarkDotNet/commit/0a734a94a13733c2950d7edbac08499c6f2c108a) feat: set RuntimeHostConfigurationOption on generated project ([#​2453](https://togithub.com/dotnet/BenchmarkDotNet/issues/2453)) (by [@​workgroupengineering](https://togithub.com/workgroupengineering)) - [ae4914](https://togithub.com/dotnet/BenchmarkDotNet/commit/ae49148a92c358676190772803fe0ed532814ce3) implement full .NET 9 support ([#​2456](https://togithub.com/dotnet/BenchmarkDotNet/issues/2456)) (by [@​adamsitnik](https://togithub.com/adamsitnik)) - [40c414](https://togithub.com/dotnet/BenchmarkDotNet/commit/40c4142734ce68bdfcbccf7086ed2b724e9428bc) Prepare v0.13.10 changelog (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Contributors (4) - Adam Sitnik ([@​adamsitnik](https://togithub.com/adamsitnik)) - Andrey Akinshin ([@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - Ankit Jain ([@​radical](https://togithub.com/radical)) - workgroupengineering ([@​workgroupengineering](https://togithub.com/workgroupengineering)) Thank you very much! ### [`v0.13.9`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.9): 0.13.9 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.9.html In the [v0.13.9](https://togithub.com/dotnet/BenchmarkDotNet/issues?q=milestone:v0.13.9) scope, 3 issues were resolved and 7 pull requests were merged. This release includes 26 commits by 5 contributors. #### Resolved issues (3) - [#​2054](https://togithub.com/dotnet/BenchmarkDotNet/issues/2054) Custom logging/visualization during the benchmark run (assignee: [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [#​2404](https://togithub.com/dotnet/BenchmarkDotNet/issues/2404) Using `DisassemblyDiagnoser` in GitHub Actions (assignee: [@​timcassell](https://togithub.com/timcassell)) - [#​2432](https://togithub.com/dotnet/BenchmarkDotNet/issues/2432) Something went wrong with outliers when using `--launchCount` (assignee: [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Merged pull requests (7) - [#​1882](https://togithub.com/dotnet/BenchmarkDotNet/pull/1882) use coalesce instead of join (by [@​askazakov](https://togithub.com/askazakov)) - [#​2413](https://togithub.com/dotnet/BenchmarkDotNet/pull/2413) Fix linux crash from disassembler (by [@​timcassell](https://togithub.com/timcassell)) - [#​2420](https://togithub.com/dotnet/BenchmarkDotNet/pull/2420) Add event processor functionality (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [#​2421](https://togithub.com/dotnet/BenchmarkDotNet/pull/2421) More nullability warnings fixes (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [#​2433](https://togithub.com/dotnet/BenchmarkDotNet/pull/2433) Fix build errors with latest sdk (by [@​timcassell](https://togithub.com/timcassell)) - [#​2434](https://togithub.com/dotnet/BenchmarkDotNet/pull/2434) Fix Event Processors not being copied in ManualConfig.Add (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [#​2435](https://togithub.com/dotnet/BenchmarkDotNet/pull/2435) Treat warnings not as errors in manual test project (by [@​timcassell](https://togithub.com/timcassell)) #### Commits (26) - [ece5cc](https://togithub.com/dotnet/BenchmarkDotNet/commit/ece5ccfc91d92b610338b05da73d2a91508e2837) Set next BenchmarkDotNet version: 0.13.9 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [ad9376](https://togithub.com/dotnet/BenchmarkDotNet/commit/ad937654174e521741aac620e16635a8ff14b1c9) Add event processor functionality ([#​2420](https://togithub.com/dotnet/BenchmarkDotNet/issues/2420)) (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [8227bb](https://togithub.com/dotnet/BenchmarkDotNet/commit/8227bbfa5f4d22c51f9c3856576d3680d8fc0a92) Address PR feedback ([#​2434](https://togithub.com/dotnet/BenchmarkDotNet/issues/2434)) (by [@​caaavik-msft](https://togithub.com/caaavik-msft)) - [46b3c0](https://togithub.com/dotnet/BenchmarkDotNet/commit/46b3c0171709c48f58966fdf2665b5f292ff6467) Fix linux crash from disassembler ([#​2413](https://togithub.com/dotnet/BenchmarkDotNet/issues/2413)) (by [@​timcassell](https://togithub.com/timcassell)) - [967a97](https://togithub.com/dotnet/BenchmarkDotNet/commit/967a975773ebd7a9744f3875220c7db8fa647957) Fix build errors with latest sdk. ([#​2433](https://togithub.com/dotnet/BenchmarkDotNet/issues/2433)) (by [@​timcassell](https://togithub.com/timcassell)) - [dd7a9b](https://togithub.com/dotnet/BenchmarkDotNet/commit/dd7a9b7cd132e522951eeb6916a3aa27a24ebf59) Treat warnings not as errors in manual test project ([#​2435](https://togithub.com/dotnet/BenchmarkDotNet/issues/2435)) (by [@​timcassell](https://togithub.com/timcassell)) - [583874](https://togithub.com/dotnet/BenchmarkDotNet/commit/58387457bd67c62fda9c831329401fe0de4ae86f) Print full stacktrace for GenerateException, see [#​2436](https://togithub.com/dotnet/BenchmarkDotNet/issues/2436) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [6e3a15](https://togithub.com/dotnet/BenchmarkDotNet/commit/6e3a159d3d3ae0d7eecc759c23a7bb0124e673df) Support WSL detection in RuntimeInformation (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8986e0](https://togithub.com/dotnet/BenchmarkDotNet/commit/8986e053c2fbc0befdef7d6e1a116a7bc83da282) Update myget url in README (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [516bd6](https://togithub.com/dotnet/BenchmarkDotNet/commit/516bd68238c38bb6f622f71039d7b91f3f33776d) Enabled nullability for BenchmarkDotNet.Diagnostics.dotTrace.csproj (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [5428eb](https://togithub.com/dotnet/BenchmarkDotNet/commit/5428ebdb8b6e9a847bb8ae6cf129b7dd9d784454) Fixed nullability warnings in methods signatures (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [7fbbc9](https://togithub.com/dotnet/BenchmarkDotNet/commit/7fbbc9f506cee0048f2ea6e7af15fbe1aa0bd7f7) Removed CanBeNull attribute (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [9d7350](https://togithub.com/dotnet/BenchmarkDotNet/commit/9d7350c21b30c2655705ede68929243846b8a407) Fixed warnings on null assignments (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [b43d28](https://togithub.com/dotnet/BenchmarkDotNet/commit/b43d280f1673526dff865f5fbfc1848c846eacdd) Fixed warnings in EngineEventLogParser (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [148165](https://togithub.com/dotnet/BenchmarkDotNet/commit/148165baf92233a3e3e67efc552e7528edb2fc78) Removed an unnecessary check (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [465aaf](https://togithub.com/dotnet/BenchmarkDotNet/commit/465aaf196a43d21b516edf6e9028c672c39937b9) Fixed empty catch warning (by [@​alinasmirnova](https://togithub.com/alinasmirnova)) - [9a7bb7](https://togithub.com/dotnet/BenchmarkDotNet/commit/9a7bb7d5d6c72a01f991d869b9106364c26b1fce) \[build] Bump: Microsoft.DocAsCode.App 2.67.5 -> Docfx.App 2.71.0 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [9dd7b6](https://togithub.com/dotnet/BenchmarkDotNet/commit/9dd7b6f4b2511bbd30ba0f6d4999f7f58cf161a6) Fix license badge link in README (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [134b8e](https://togithub.com/dotnet/BenchmarkDotNet/commit/134b8edd09ad7dad0a17728eae4e9f50e16d3fe0) \[build] Automatic NextVersion evaluation (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8090d9](https://togithub.com/dotnet/BenchmarkDotNet/commit/8090d995e847c3c3d84db1fd5acbee312a75cf81) Suppress NETSDK1138 (TFM out of support warning) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [af610e](https://togithub.com/dotnet/BenchmarkDotNet/commit/af610eec251bfa74f7317eaec915df9b905c979b) Bump .NET SDK: 7.0.305->7.0.401 (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [8838ed](https://togithub.com/dotnet/BenchmarkDotNet/commit/8838ed4bf74377642d32774c558c0955e67c0faf) \[build] Fix docfx build warnings (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [2d379b](https://togithub.com/dotnet/BenchmarkDotNet/commit/2d379b37310983dbe645a2129066d9af65d9e0d7) Remove outlier consistency check, fix [#​2432](https://togithub.com/dotnet/BenchmarkDotNet/issues/2432) (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [00628a](https://togithub.com/dotnet/BenchmarkDotNet/commit/00628ab31b79a78e1c22c298ca0086bdf28065a7) use coalesce instead of join (by [@​askazakov](https://togithub.com/askazakov)) - [411a6e](https://togithub.com/dotnet/BenchmarkDotNet/commit/411a6e7594c45c9ffa55b0b6caecb7f6ed1b3081) Prepare v0.13.9 changelog (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - [228a46](https://togithub.com/dotnet/BenchmarkDotNet/commit/228a464e8be6c580ad9408e98f18813f6407fb5a) Rollback docfx.json (by [@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) #### Contributors (5) - Alina Smirnova ([@​alinasmirnova](https://togithub.com/alinasmirnova)) - Andrey Akinshin ([@​AndreyAkinshin](https://togithub.com/AndreyAkinshin)) - askazakov ([@​askazakov](https://togithub.com/askazakov)) - Cameron Aavik ([@​caaavik-msft](https://togithub.com/caaavik-msft)) - Tim Cassell ([@​timcassell](https://togithub.com/timcassell)) Thank you very much! ### [`v0.13.8`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.8): 0.13.8 Full changelog: https://benchmarkdotnet.org/changelog/v0.13.8.html #### Highlights This release contains important bug fixes. #### What's Changed - Issue2394 multiple markdown exporters not possible even with different names by [@​bstordrup](https://togithub.com/bstordrup) in [https://github.com/dotnet/BenchmarkDotNet/pull/2395](https://togithub.com/dotnet/BenchmarkDotNet/pull/2395) - Make MarkdownExporter ctor and Dialect protected by [@​nietras](https://togithub.com/nietras) in [https://github.com/dotnet/BenchmarkDotNet/pull/2407](https://togithub.com/dotnet/BenchmarkDotNet/pull/2407) - Refactor out base TextLogger from StreamLogger by [@​nietras](https://togithub.com/nietras) in [https://github.com/dotnet/BenchmarkDotNet/pull/2406](https://togithub.com/dotnet/BenchmarkDotNet/pull/2406) - - update the templates install command to reflect dotnet cli updates by [@​baywet](https://togithub.com/baywet) in [https://github.com/dotnet/BenchmarkDotNet/pull/2415](https://togithub.com/dotnet/BenchmarkDotNet/pull/2415) - Update stub decoding for .NET 8 for disassemblers by [@​janvorli](https://togithub.com/janvorli) in [https://github.com/dotnet/BenchmarkDotNet/pull/2416](https://togithub.com/dotnet/BenchmarkDotNet/pull/2416) - Enable nullability for BenchmarkDotNet.Annotations by [@​alinasmirnova](https://togithub.com/alinasmirnova) in [https://github.com/dotnet/BenchmarkDotNet/pull/2418](https://togithub.com/dotnet/BenchmarkDotNet/pull/2418) - Nullability In BenchmarkDotNet project by [@​alinasmirnova](https://togithub.com/alinasmirnova) in [https://github.com/dotnet/BenchmarkDotNet/pull/2419](https://togithub.com/dotnet/BenchmarkDotNet/pull/2419) - feat: add text justification style by [@​Vahdanian](https://togithub.com/Vahdanian) in [https://github.com/dotnet/BenchmarkDotNet/pull/2410](https://togithub.com/dotnet/BenchmarkDotNet/pull/2410) - Default to RoslynToolchain by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2409](https://togithub.com/dotnet/BenchmarkDotNet/pull/2409) #### New Contributors - [@​bstordrup](https://togithub.com/bstordrup) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2395](https://togithub.com/dotnet/BenchmarkDotNet/pull/2395) - [@​baywet](https://togithub.com/baywet) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2415](https://togithub.com/dotnet/BenchmarkDotNet/pull/2415) - [@​Vahdanian](https://togithub.com/Vahdanian) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2410](https://togithub.com/dotnet/BenchmarkDotNet/pull/2410) **Full Changelog**: https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.7...v0.13.8 ### [`v0.13.7`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.7): BenchmarkDotNet v0.13.7 This release contains a bunch of important bug fixes. Full changelog: https://benchmarkdotnet.org/changelog/v0.13.7.html #### What's Changed - Improve build for mono aot by [@​radical](https://togithub.com/radical) in [https://github.com/dotnet/BenchmarkDotNet/pull/2367](https://togithub.com/dotnet/BenchmarkDotNet/pull/2367) - IComparable fallback for Tuple/ValueTuple by [@​mrahhal](https://togithub.com/mrahhal) in [https://github.com/dotnet/BenchmarkDotNet/pull/2368](https://togithub.com/dotnet/BenchmarkDotNet/pull/2368) - Don't copy `PackageReference` in csproj by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2365](https://togithub.com/dotnet/BenchmarkDotNet/pull/2365) - Fix regression in parsing arguments with spaces Closes [#​2373](https://togithub.com/dotnet/BenchmarkDotNet/issues/2373) by [@​kant2002](https://togithub.com/kant2002) in [https://github.com/dotnet/BenchmarkDotNet/pull/2375](https://togithub.com/dotnet/BenchmarkDotNet/pull/2375) - `AggressiveOptimization` in `InProcess` toolchains by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2335](https://togithub.com/dotnet/BenchmarkDotNet/pull/2335) - Add expected results tests by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2361](https://togithub.com/dotnet/BenchmarkDotNet/pull/2361) - \[chore]: fix error message by [@​BurakTaner](https://togithub.com/BurakTaner) in [https://github.com/dotnet/BenchmarkDotNet/pull/2379](https://togithub.com/dotnet/BenchmarkDotNet/pull/2379) - Cancel old jobs on push by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2380](https://togithub.com/dotnet/BenchmarkDotNet/pull/2380) - Support `--cli` argument for `CsProjClassicNetToolchain` by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2381](https://togithub.com/dotnet/BenchmarkDotNet/pull/2381) - Rebuild .Net Framework projects by [@​timcassell](https://togithub.com/timcassell) in [https://github.com/dotnet/BenchmarkDotNet/pull/2370](https://togithub.com/dotnet/BenchmarkDotNet/pull/2370) - Fix missing import on Debug build by [@​caaavik-msft](https://togithub.com/caaavik-msft) in [https://github.com/dotnet/BenchmarkDotNet/pull/2385](https://togithub.com/dotnet/BenchmarkDotNet/pull/2385) - perfcollect: don't restore symbols for local builds by [@​adamsitnik](https://togithub.com/adamsitnik) in [https://github.com/dotnet/BenchmarkDotNet/pull/2384](https://togithub.com/dotnet/BenchmarkDotNet/pull/2384) - Fix PlatformNotSupportedException thrown on Android in ConsoleTitler by [@​Adam--](https://togithub.com/Adam--) in [https://github.com/dotnet/BenchmarkDotNet/pull/2390](https://togithub.com/dotnet/BenchmarkDotNet/pull/2390) #### New Contributors - [@​BurakTaner](https://togithub.com/BurakTaner) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2379](https://togithub.com/dotnet/BenchmarkDotNet/pull/2379) - [@​caaavik-msft](https://togithub.com/caaavik-msft) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2385](https://togithub.com/dotnet/BenchmarkDotNet/pull/2385) - [@​Adam--](https://togithub.com/Adam--) made their first contribution in [https://github.com/dotnet/BenchmarkDotNet/pull/2390](https://togithub.com/dotnet/BenchmarkDotNet/pull/2390) **Full Changelog**: https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.6...v0.13.7 ### [`v0.13.6`](https://togithub.com/dotnet/BenchmarkDotNet/releases/tag/v0.13.6): BenchmarkDotNet v0.13.6 #### Highlights - New [BenchmarkDotNet.Diagnostics.dotTrace](https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace) NuGet package. Once this package is installed, you can annotate your benchmarks with the `[DotTraceDiagnoser]` and get a [dotTrace](https://www.jetbrains.com/profiler/) performance snapshot at the end of the benchmark run. [#​2328](https://togithub.com/dotnet/BenchmarkDotNet/pull/2328) - Updated documentation website. We migrated to [docfx](https://dotnet.github.io/docfx/) 2.67 and got the refreshed modern template based on bootstrap 5 with dark/light theme switcher. - Updated [BenchmarkDotNet.Templates](https://www.nuget.org/packages/BenchmarkDotNet.Templates). Multiple issues were resolved, now you can create new benchmark projects from terminal or your favorite IDE. [#​1658](https://togithub.com/dotnet/BenchmarkDotNet/issues/1658) [#​1881](https://togithub.com/dotnet/BenchmarkDotNet/issues/1881) [#​2149](https://togithub.com/dotnet/BenchmarkDotNet/issues/2149) [#​2338](https://togithub.com/dotnet/BenchmarkDotNet/pull/2338) - Response file support. Now it's possible to pass additional arguments to BenchmarkDotNet using `@filename` syntax. [#​2320](https://togithub.com/dotnet/BenchmarkDotNet/pull/2320) [#​2348](https://togithub.com/dotnet/BenchmarkDotNet/pull/2348) - Custom runtime support. [#​2285](https://togithub.com/dotnet/BenchmarkDotNet/pull/2285) - Introduce CategoryDiscoverer, see [`IntroCategoryDiscoverer`](xref:BenchmarkDotNet.Samples.IntroCategoryDiscoverer). [#​2306](https://togithub.com/dotnet/BenchmarkDotNet/issues/2306) [#​2307](https://togithub.com/dotnet/BenchmarkDotNet/pull/2307) - Multiple bug fixes. Full changelog: https://benchmarkdotnet.org/changelog/v0.13.6.html
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b443a8af..f8a4159c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + From 84f8e18c4a12f8290181df9f7696d7c7b567348e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:03:17 -0400 Subject: [PATCH 053/123] chore(deps): update dependency dotnet-sdk to v8.0.400 (#295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [dotnet-sdk](https://togithub.com/dotnet/sdk) | dotnet-sdk | patch | `8.0.303` -> `8.0.400` | --- ### Release Notes
dotnet/sdk (dotnet-sdk) ### [`v8.0.400`](https://togithub.com/dotnet/sdk/compare/v8.0.303...v8.0.400) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.304...v8.0.400) ### [`v8.0.304`](https://togithub.com/dotnet/sdk/compare/v8.0.303...v8.0.304) [Compare Source](https://togithub.com/dotnet/sdk/compare/v8.0.303...v8.0.304)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 9f8f3618..bc3a25e7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.303" + "version": "8.0.400" } } From 94f4d66b182b1cebd9ae2532ca8b5192c7f66e72 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 21 Aug 2024 11:36:05 -0400 Subject: [PATCH 054/123] chore: in-memory UpdateFlags to UpdateFlagsAsync (#298) I was doing an audit before the release and found this one method could use a suffix update. Signed-off-by: Todd Baert Signed-off-by: Artyom Tonoyan --- src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 4 ++-- .../Providers/Memory/InMemoryProviderTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 771e2210..3283ea22 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -43,10 +43,10 @@ public InMemoryProvider(IDictionary? flags = null) } /// - /// Updating provider flags configuration, replacing all flags. + /// Update provider flag configuration, replacing all flags. /// /// the flags to use instead of the previous flags. - public async Task UpdateFlags(IDictionary? flags = null) + public async Task UpdateFlagsAsync(IDictionary? flags = null) { var changed = this._flags.Keys.ToList(); if (flags == null) diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 83974c23..c83ce0ce 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -170,7 +170,7 @@ public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( public async Task EmptyFlags_ShouldWork() { var provider = new InMemoryProvider(); - await provider.UpdateFlags(); + await provider.UpdateFlagsAsync(); Assert.Equal("InMemory", provider.GetMetadata().Name); } @@ -216,7 +216,7 @@ public async Task PutConfiguration_shouldUpdateConfigAndRunHandlers() Assert.True(details.Value); // update flags - await provider.UpdateFlags(new Dictionary(){ + await provider.UpdateFlagsAsync(new Dictionary(){ { "new-flag", new Flag( variants: new Dictionary(){ From 756a7f75c4a449b62ba5347656661635a2bbd205 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:50:07 -0400 Subject: [PATCH 055/123] chore(main): release 2.0.0 (#254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit :robot: I have created a release *beep* *boop* --- ## [2.0.0](https://github.com/open-feature/dotnet-sdk/compare/v1.5.0...v2.0.0) (2024-08-21) Today we're announcing the release of the OpenFeature SDK for .NET, v2.0! This release contains several ergonomic improvements to the SDK, which .NET developers will appreciate. It also includes some performance optimizations brought to you by the latest .NET primitives. For details and migration tips, check out: https://openfeature.dev/blog/dotnet-sdk-v2 ### ⚠ BREAKING CHANGES * domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) * internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) * add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) * Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) * Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) ### 🐛 Bug Fixes * Add missing error message when an error occurred ([#256](https://github.com/open-feature/dotnet-sdk/issues/256)) ([949d53c](https://github.com/open-feature/dotnet-sdk/commit/949d53cada68bee8e80d113357fa6df8d425d3c1)) * Should map metadata when converting from ResolutionDetails to FlagEvaluationDetails ([#282](https://github.com/open-feature/dotnet-sdk/issues/282)) ([2f8bd21](https://github.com/open-feature/dotnet-sdk/commit/2f8bd2179ec35f79cbbab77206de78dd9b0f58d6)) ### ✨ New Features * add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) ([33154d2](https://github.com/open-feature/dotnet-sdk/commit/33154d2ed6b0b27f4a86a5fbad440a784a89c881)) * back targetingKey with internal map ([#287](https://github.com/open-feature/dotnet-sdk/issues/287)) ([ccc2f7f](https://github.com/open-feature/dotnet-sdk/commit/ccc2f7fbd4e4f67eb03c2e6a07140ca31225da2c)) * domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) ([4c0592e](https://github.com/open-feature/dotnet-sdk/commit/4c0592e6baf86d831fc7b39762c960ca0dd843a9)) * Drop net7 TFM ([#284](https://github.com/open-feature/dotnet-sdk/issues/284)) ([2dbe1f4](https://github.com/open-feature/dotnet-sdk/commit/2dbe1f4c95aeae501c8b5154b1ccefafa7df2632)) * internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) ([63faa84](https://github.com/open-feature/dotnet-sdk/commit/63faa8440cd650b0bd6c3ec009ad9bd78bc31f32)) * Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) ([ac7d7de](https://github.com/open-feature/dotnet-sdk/commit/ac7d7debf50cef08668bcd9457d3f830b8718806)) ### 🧹 Chore * cleanup code ([#277](https://github.com/open-feature/dotnet-sdk/issues/277)) ([44cf586](https://github.com/open-feature/dotnet-sdk/commit/44cf586f96607716fb8b4464d81edfd6074f7376)) * **deps:** Project file cleanup and remove unnecessary dependencies ([#251](https://github.com/open-feature/dotnet-sdk/issues/251)) ([79def47](https://github.com/open-feature/dotnet-sdk/commit/79def47106b19b316b691fa195f7160ddcfb9a41)) * **deps:** update actions/upload-artifact action to v4.3.3 ([#263](https://github.com/open-feature/dotnet-sdk/issues/263)) ([7718649](https://github.com/open-feature/dotnet-sdk/commit/77186495cd3d567b0aabd418f23a65567656b54d)) * **deps:** update actions/upload-artifact action to v4.3.4 ([#278](https://github.com/open-feature/dotnet-sdk/issues/278)) ([15189f1](https://github.com/open-feature/dotnet-sdk/commit/15189f1c6f7eb0931036e022eed68f58a1110b5b)) * **deps:** update actions/upload-artifact action to v4.3.5 ([#291](https://github.com/open-feature/dotnet-sdk/issues/291)) ([00e99d6](https://github.com/open-feature/dotnet-sdk/commit/00e99d6c2208b304748d00a931f460d6d6aab4de)) * **deps:** update codecov/codecov-action action to v4 ([#227](https://github.com/open-feature/dotnet-sdk/issues/227)) ([11a0333](https://github.com/open-feature/dotnet-sdk/commit/11a03332726f07dd0327d222e6bd6e1843db460c)) * **deps:** update codecov/codecov-action action to v4.3.1 ([#267](https://github.com/open-feature/dotnet-sdk/issues/267)) ([ff9df59](https://github.com/open-feature/dotnet-sdk/commit/ff9df593400f92c016eee1a45bd7097da008d4dc)) * **deps:** update codecov/codecov-action action to v4.5.0 ([#272](https://github.com/open-feature/dotnet-sdk/issues/272)) ([281295d](https://github.com/open-feature/dotnet-sdk/commit/281295d2999e4d36c5a2078cbfdfe5e59f4652b2)) * **deps:** update dependency benchmarkdotnet to v0.14.0 ([#293](https://github.com/open-feature/dotnet-sdk/issues/293)) ([aec222f](https://github.com/open-feature/dotnet-sdk/commit/aec222fe1b1a5b52f8349ceb98c12b636eb155eb)) * **deps:** update dependency coverlet.collector to v6.0.2 ([#247](https://github.com/open-feature/dotnet-sdk/issues/247)) ([ab34c16](https://github.com/open-feature/dotnet-sdk/commit/ab34c16b513ddbd0a53e925baaccd088163fbcc8)) * **deps:** update dependency coverlet.msbuild to v6.0.2 ([#239](https://github.com/open-feature/dotnet-sdk/issues/239)) ([e654222](https://github.com/open-feature/dotnet-sdk/commit/e6542222827cc25cd5a1acc5af47ce55149c0623)) * **deps:** update dependency dotnet-sdk to v8.0.204 ([#261](https://github.com/open-feature/dotnet-sdk/issues/261)) ([8f82645](https://github.com/open-feature/dotnet-sdk/commit/8f8264520814a42b7ed2af8f70340e7673259b6f)) * **deps:** update dependency dotnet-sdk to v8.0.301 ([#271](https://github.com/open-feature/dotnet-sdk/issues/271)) ([acd0385](https://github.com/open-feature/dotnet-sdk/commit/acd0385641e114a16d0ee56e3a143baa7d3c0535)) * **deps:** update dependency dotnet-sdk to v8.0.303 ([#275](https://github.com/open-feature/dotnet-sdk/issues/275)) ([871dcac](https://github.com/open-feature/dotnet-sdk/commit/871dcacc94fa2abb10434616c469cad6f674f07a)) * **deps:** update dependency dotnet-sdk to v8.0.400 ([#295](https://github.com/open-feature/dotnet-sdk/issues/295)) ([bb4f352](https://github.com/open-feature/dotnet-sdk/commit/bb4f3526c2c2c2ca48ae61e883d6962847ebc5a6)) * **deps:** update dependency githubactionstestlogger to v2.4.1 ([#274](https://github.com/open-feature/dotnet-sdk/issues/274)) ([46c2b15](https://github.com/open-feature/dotnet-sdk/commit/46c2b153c848bd3a500b828ddb89bd3b07753bf1)) * **deps:** update dependency microsoft.net.test.sdk to v17.10.0 ([#273](https://github.com/open-feature/dotnet-sdk/issues/273)) ([581ff81](https://github.com/open-feature/dotnet-sdk/commit/581ff81c7b1840c34840229bf20444c528c64cc6)) * **deps:** update dotnet monorepo ([#218](https://github.com/open-feature/dotnet-sdk/issues/218)) ([bc8301d](https://github.com/open-feature/dotnet-sdk/commit/bc8301d1c54e0b48ede3235877d969f28d61fb29)) * **deps:** update xunit-dotnet monorepo ([#262](https://github.com/open-feature/dotnet-sdk/issues/262)) ([43f14cc](https://github.com/open-feature/dotnet-sdk/commit/43f14cca072372ecacec89a949c85f763c1ee7b4)) * **deps:** update xunit-dotnet monorepo ([#279](https://github.com/open-feature/dotnet-sdk/issues/279)) ([fb1cc66](https://github.com/open-feature/dotnet-sdk/commit/fb1cc66440dd6bdbbef1ac1f85bf3228b80073af)) * **deps:** update xunit-dotnet monorepo to v2.8.1 ([#266](https://github.com/open-feature/dotnet-sdk/issues/266)) ([a7b6d85](https://github.com/open-feature/dotnet-sdk/commit/a7b6d8561716763f324325a8803b913c4d69c044)) * Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) ([5a5312c](https://github.com/open-feature/dotnet-sdk/commit/5a5312cc082ccd880b65165135e05b4f3b035df7)) * in-memory UpdateFlags to UpdateFlagsAsync ([#298](https://github.com/open-feature/dotnet-sdk/issues/298)) ([390205a](https://github.com/open-feature/dotnet-sdk/commit/390205a41d29d786b5f41b0d91f34ec237276cb4)) * prompt 2.0 ([9b9c3fd](https://github.com/open-feature/dotnet-sdk/commit/9b9c3fd09c27b191104d7ceaa726b6edd71fcd06)) * Support for determining spec support for the repo ([#270](https://github.com/open-feature/dotnet-sdk/issues/270)) ([67a1a0a](https://github.com/open-feature/dotnet-sdk/commit/67a1a0aea95ee943976990b1d1782e4061300b50)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Signed-off-by: Todd Baert Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- .release-please-manifest.json | 2 +- CHANGELOG.md | 58 +++ README.md | 644 +++++++++++++++++----------------- build/Common.prod.props | 2 +- version.txt | 2 +- 5 files changed, 383 insertions(+), 325 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index dd8fde77..895bf0e3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0" + ".": "2.0.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 929d2c66..fb316f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## [2.0.0](https://github.com/open-feature/dotnet-sdk/compare/v1.5.0...v2.0.0) (2024-08-21) + +Today we're announcing the release of the OpenFeature SDK for .NET, v2.0! This release contains several ergonomic improvements to the SDK, which .NET developers will appreciate. It also includes some performance optimizations brought to you by the latest .NET primitives. + +For details and migration tips, check out: https://openfeature.dev/blog/dotnet-sdk-v2 + +### ⚠ BREAKING CHANGES + +* domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) +* internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) +* add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) +* Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) +* Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) + +### 🐛 Bug Fixes + +* Add missing error message when an error occurred ([#256](https://github.com/open-feature/dotnet-sdk/issues/256)) ([949d53c](https://github.com/open-feature/dotnet-sdk/commit/949d53cada68bee8e80d113357fa6df8d425d3c1)) +* Should map metadata when converting from ResolutionDetails to FlagEvaluationDetails ([#282](https://github.com/open-feature/dotnet-sdk/issues/282)) ([2f8bd21](https://github.com/open-feature/dotnet-sdk/commit/2f8bd2179ec35f79cbbab77206de78dd9b0f58d6)) + + +### ✨ New Features + +* add CancellationTokens, ValueTasks hooks ([#268](https://github.com/open-feature/dotnet-sdk/issues/268)) ([33154d2](https://github.com/open-feature/dotnet-sdk/commit/33154d2ed6b0b27f4a86a5fbad440a784a89c881)) +* back targetingKey with internal map ([#287](https://github.com/open-feature/dotnet-sdk/issues/287)) ([ccc2f7f](https://github.com/open-feature/dotnet-sdk/commit/ccc2f7fbd4e4f67eb03c2e6a07140ca31225da2c)) +* domain instead of client name ([#294](https://github.com/open-feature/dotnet-sdk/issues/294)) ([4c0592e](https://github.com/open-feature/dotnet-sdk/commit/4c0592e6baf86d831fc7b39762c960ca0dd843a9)) +* Drop net7 TFM ([#284](https://github.com/open-feature/dotnet-sdk/issues/284)) ([2dbe1f4](https://github.com/open-feature/dotnet-sdk/commit/2dbe1f4c95aeae501c8b5154b1ccefafa7df2632)) +* internally maintain provider status ([#276](https://github.com/open-feature/dotnet-sdk/issues/276)) ([63faa84](https://github.com/open-feature/dotnet-sdk/commit/63faa8440cd650b0bd6c3ec009ad9bd78bc31f32)) +* Use same type for flag metadata and event metadata ([#241](https://github.com/open-feature/dotnet-sdk/issues/241)) ([ac7d7de](https://github.com/open-feature/dotnet-sdk/commit/ac7d7debf50cef08668bcd9457d3f830b8718806)) + + +### 🧹 Chore + +* cleanup code ([#277](https://github.com/open-feature/dotnet-sdk/issues/277)) ([44cf586](https://github.com/open-feature/dotnet-sdk/commit/44cf586f96607716fb8b4464d81edfd6074f7376)) +* **deps:** Project file cleanup and remove unnecessary dependencies ([#251](https://github.com/open-feature/dotnet-sdk/issues/251)) ([79def47](https://github.com/open-feature/dotnet-sdk/commit/79def47106b19b316b691fa195f7160ddcfb9a41)) +* **deps:** update actions/upload-artifact action to v4.3.3 ([#263](https://github.com/open-feature/dotnet-sdk/issues/263)) ([7718649](https://github.com/open-feature/dotnet-sdk/commit/77186495cd3d567b0aabd418f23a65567656b54d)) +* **deps:** update actions/upload-artifact action to v4.3.4 ([#278](https://github.com/open-feature/dotnet-sdk/issues/278)) ([15189f1](https://github.com/open-feature/dotnet-sdk/commit/15189f1c6f7eb0931036e022eed68f58a1110b5b)) +* **deps:** update actions/upload-artifact action to v4.3.5 ([#291](https://github.com/open-feature/dotnet-sdk/issues/291)) ([00e99d6](https://github.com/open-feature/dotnet-sdk/commit/00e99d6c2208b304748d00a931f460d6d6aab4de)) +* **deps:** update codecov/codecov-action action to v4 ([#227](https://github.com/open-feature/dotnet-sdk/issues/227)) ([11a0333](https://github.com/open-feature/dotnet-sdk/commit/11a03332726f07dd0327d222e6bd6e1843db460c)) +* **deps:** update codecov/codecov-action action to v4.3.1 ([#267](https://github.com/open-feature/dotnet-sdk/issues/267)) ([ff9df59](https://github.com/open-feature/dotnet-sdk/commit/ff9df593400f92c016eee1a45bd7097da008d4dc)) +* **deps:** update codecov/codecov-action action to v4.5.0 ([#272](https://github.com/open-feature/dotnet-sdk/issues/272)) ([281295d](https://github.com/open-feature/dotnet-sdk/commit/281295d2999e4d36c5a2078cbfdfe5e59f4652b2)) +* **deps:** update dependency benchmarkdotnet to v0.14.0 ([#293](https://github.com/open-feature/dotnet-sdk/issues/293)) ([aec222f](https://github.com/open-feature/dotnet-sdk/commit/aec222fe1b1a5b52f8349ceb98c12b636eb155eb)) +* **deps:** update dependency coverlet.collector to v6.0.2 ([#247](https://github.com/open-feature/dotnet-sdk/issues/247)) ([ab34c16](https://github.com/open-feature/dotnet-sdk/commit/ab34c16b513ddbd0a53e925baaccd088163fbcc8)) +* **deps:** update dependency coverlet.msbuild to v6.0.2 ([#239](https://github.com/open-feature/dotnet-sdk/issues/239)) ([e654222](https://github.com/open-feature/dotnet-sdk/commit/e6542222827cc25cd5a1acc5af47ce55149c0623)) +* **deps:** update dependency dotnet-sdk to v8.0.204 ([#261](https://github.com/open-feature/dotnet-sdk/issues/261)) ([8f82645](https://github.com/open-feature/dotnet-sdk/commit/8f8264520814a42b7ed2af8f70340e7673259b6f)) +* **deps:** update dependency dotnet-sdk to v8.0.301 ([#271](https://github.com/open-feature/dotnet-sdk/issues/271)) ([acd0385](https://github.com/open-feature/dotnet-sdk/commit/acd0385641e114a16d0ee56e3a143baa7d3c0535)) +* **deps:** update dependency dotnet-sdk to v8.0.303 ([#275](https://github.com/open-feature/dotnet-sdk/issues/275)) ([871dcac](https://github.com/open-feature/dotnet-sdk/commit/871dcacc94fa2abb10434616c469cad6f674f07a)) +* **deps:** update dependency dotnet-sdk to v8.0.400 ([#295](https://github.com/open-feature/dotnet-sdk/issues/295)) ([bb4f352](https://github.com/open-feature/dotnet-sdk/commit/bb4f3526c2c2c2ca48ae61e883d6962847ebc5a6)) +* **deps:** update dependency githubactionstestlogger to v2.4.1 ([#274](https://github.com/open-feature/dotnet-sdk/issues/274)) ([46c2b15](https://github.com/open-feature/dotnet-sdk/commit/46c2b153c848bd3a500b828ddb89bd3b07753bf1)) +* **deps:** update dependency microsoft.net.test.sdk to v17.10.0 ([#273](https://github.com/open-feature/dotnet-sdk/issues/273)) ([581ff81](https://github.com/open-feature/dotnet-sdk/commit/581ff81c7b1840c34840229bf20444c528c64cc6)) +* **deps:** update dotnet monorepo ([#218](https://github.com/open-feature/dotnet-sdk/issues/218)) ([bc8301d](https://github.com/open-feature/dotnet-sdk/commit/bc8301d1c54e0b48ede3235877d969f28d61fb29)) +* **deps:** update xunit-dotnet monorepo ([#262](https://github.com/open-feature/dotnet-sdk/issues/262)) ([43f14cc](https://github.com/open-feature/dotnet-sdk/commit/43f14cca072372ecacec89a949c85f763c1ee7b4)) +* **deps:** update xunit-dotnet monorepo ([#279](https://github.com/open-feature/dotnet-sdk/issues/279)) ([fb1cc66](https://github.com/open-feature/dotnet-sdk/commit/fb1cc66440dd6bdbbef1ac1f85bf3228b80073af)) +* **deps:** update xunit-dotnet monorepo to v2.8.1 ([#266](https://github.com/open-feature/dotnet-sdk/issues/266)) ([a7b6d85](https://github.com/open-feature/dotnet-sdk/commit/a7b6d8561716763f324325a8803b913c4d69c044)) +* Enable nullable reference types ([#253](https://github.com/open-feature/dotnet-sdk/issues/253)) ([5a5312c](https://github.com/open-feature/dotnet-sdk/commit/5a5312cc082ccd880b65165135e05b4f3b035df7)) +* in-memory UpdateFlags to UpdateFlagsAsync ([#298](https://github.com/open-feature/dotnet-sdk/issues/298)) ([390205a](https://github.com/open-feature/dotnet-sdk/commit/390205a41d29d786b5f41b0d91f34ec237276cb4)) +* prompt 2.0 ([9b9c3fd](https://github.com/open-feature/dotnet-sdk/commit/9b9c3fd09c27b191104d7ceaa726b6edd71fcd06)) +* Support for determining spec support for the repo ([#270](https://github.com/open-feature/dotnet-sdk/issues/270)) ([67a1a0a](https://github.com/open-feature/dotnet-sdk/commit/67a1a0aea95ee943976990b1d1782e4061300b50)) + ## [1.5.0](https://github.com/open-feature/dotnet-sdk/compare/v1.4.1...v1.5.0) (2024-03-12) diff --git a/README.md b/README.md index bce047f8..b8f25012 100644 --- a/README.md +++ b/README.md @@ -1,322 +1,322 @@ - - - -![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) - -## .NET SDK - - - -[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) -[ - ![Release](https://img.shields.io/static/v1?label=release&message=v1.5.0&color=blue&style=for-the-badge) -](https://github.com/open-feature/dotnet-sdk/releases/tag/v1.5.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) -[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) - - -[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. - - - -## 🚀 Quick start - -### Requirements - -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ - -Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -### Install - -Use the following to initialize your project: - -```sh -dotnet new console -``` - -and install OpenFeature: - -```sh -dotnet add package OpenFeature -``` - -### Usage - -```csharp -public async Task Example() -{ - // Register your feature flag provider - await Api.Instance.SetProviderAsync(new InMemoryProvider()); - - // Create a new client - FeatureClient client = Api.Instance.GetClient(); - - // Evaluate your feature flag - bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false); - - if ( v2Enabled ) - { - //Do some work - } -} -``` - -## 🌟 Features - -| Status | Features | Description | -| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| ✅ | [Logging](#logging) | Integrate with popular logging packages. | -| ✅ | [Domains](#domains) | Logically bind clients with providers. | -| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | - -> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ - -### Providers - -[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). - -If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. - -Once you've added a provider as a dependency, it can be registered with OpenFeature like this: - -```csharp -await Api.Instance.SetProviderAsync(new MyProvider()); -``` - -In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [domains](#domains), which is covered in more detail below. - -### Targeting - -Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. -In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). -If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - -```csharp -// set a value to the global context -EvaluationContextBuilder builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext apiCtx = builder.Build(); -Api.Instance.SetContext(apiCtx); - -// set a value to the client context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext clientCtx = builder.Build(); -var client = Api.Instance.GetClient(); -client.SetContext(clientCtx); - -// set a value to the invocation context -builder = EvaluationContext.Builder(); -builder.Set("region", "us-east-1"); -EvaluationContext reqCtx = builder.Build(); - -bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); - -``` - -### Hooks - -[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. -If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. - -Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. - -```csharp -// add a hook globally, to run on all evaluations -Api.Instance.AddHooks(new ExampleGlobalHook()); - -// add a hook on this client, to run on all evaluations made by this client -var client = Api.Instance.GetClient(); -client.AddHooks(new ExampleClientHook()); - -// add a hook for this evaluation only -var value = await client.GetBooleanValueAsync("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); -``` - -### Logging - -The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. - -### Domains - -Clients can be assigned to a domain. -A domain is a logical identifier which can be used to associate clients with a particular provider. -If a domain has no associated provider, the default provider is used. - -```csharp -// registering the default provider -await Api.Instance.SetProviderAsync(new LocalProvider()); - -// registering a provider to a domain -await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); - -// a client backed by default provider -FeatureClient clientDefault = Api.Instance.GetClient(); - -// a client backed by CachedProvider -FeatureClient scopedClient = Api.Instance.GetClient("clientForCache"); -``` - -Domains can be defined on a provider during registration. -For more details, please refer to the [providers](#providers) section. - -### Eventing - -Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, -provider readiness, or error conditions. -Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. -Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. - -Please refer to the documentation of the provider you're using to see what events are supported. - -Example usage of an Event handler: - -```csharp -public static void EventHandler(ProviderEventPayload eventDetails) -{ - Console.WriteLine(eventDetails.Type); -} -``` - -```csharp -EventHandlerDelegate callback = EventHandler; -// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event -Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -It is also possible to register an event handler for a specific client, as in the following example: - -```csharp -EventHandlerDelegate callback = EventHandler; - -var myClient = Api.Instance.GetClient("my-client"); - -var provider = new ExampleProvider(); -await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider); - -myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); -``` - -### Shutdown - -The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. - -```csharp -// Shut down all providers -await Api.Instance.ShutdownAsync(); -``` - -## Extending - -### Develop a provider - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. - -```csharp -public class MyProvider : FeatureProvider -{ - public override Metadata GetMetadata() - { - return new Metadata("My Provider"); - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve a boolean flag value - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve a string flag value - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) - { - // resolve an int flag value - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve a double flag value - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - // resolve an object flag value - } -} -``` - -### Develop a hook - -To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. -Implement your own hook by conforming to the `Hook interface`. -To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. - -```csharp -public class MyHook : Hook -{ - public ValueTask BeforeAsync(HookContext context, - IReadOnlyDictionary hints = null) - { - // code to run before flag evaluation - } - - public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, - IReadOnlyDictionary hints = null) - { - // code to run after successful flag evaluation - } - - public ValueTask ErrorAsync(HookContext context, Exception error, - IReadOnlyDictionary hints = null) - { - // code to run if there's an error during before hooks or during flag evaluation - } - - public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) - { - // code to run after all other stages, regardless of success/failure - } -} -``` - -Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! - - -## ⭐️ Support the project - -- Give this repo a ⭐️! -- Follow us on social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more information, check out our [community page](https://openfeature.dev/community/) - -## 🤝 Contributing - -Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. - -### Thanks to everyone who has already contributed - -[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) - -Made with [contrib.rocks](https://contrib.rocks). - + + + +![OpenFeature Dark Logo](https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg) + +## .NET SDK + + + +[![Specification](https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge)](https://github.com/open-feature/spec/releases/tag/v0.7.0) +[ + ![Release](https://img.shields.io/static/v1?label=release&message=v2.0.0&color=blue&style=for-the-badge) +](https://github.com/open-feature/dotnet-sdk/releases/tag/v2.0.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) +[![NuGet](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://www.bestpractices.dev/en/projects/6250) + + +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. + + + +## 🚀 Quick start + +### Requirements + +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ + +Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 + +### Install + +Use the following to initialize your project: + +```sh +dotnet new console +``` + +and install OpenFeature: + +```sh +dotnet add package OpenFeature +``` + +### Usage + +```csharp +public async Task Example() +{ + // Register your feature flag provider + await Api.Instance.SetProviderAsync(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } +} +``` + +## 🌟 Features + +| Status | Features | Description | +| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Domains](#domains) | Logically bind clients with providers. | +| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +> Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ + +### Providers + +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). + +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```csharp +await Api.Instance.SetProviderAsync(new MyProvider()); +``` + +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [domains](#domains), which is covered in more detail below. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). + +```csharp +// set a value to the global context +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); + +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); + +bool flagValue = await client.GetBooleanValuAsync("some-flag", false, reqCtx); + +``` + +### Hooks + +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks. +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```csharp +// add a hook globally, to run on all evaluations +Api.Instance.AddHooks(new ExampleGlobalHook()); + +// add a hook on this client, to run on all evaluations made by this client +var client = Api.Instance.GetClient(); +client.AddHooks(new ExampleClientHook()); + +// add a hook for this evaluation only +var value = await client.GetBooleanValueAsync("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); +``` + +### Logging + +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. + +### Domains + +Clients can be assigned to a domain. +A domain is a logical identifier which can be used to associate clients with a particular provider. +If a domain has no associated provider, the default provider is used. + +```csharp +// registering the default provider +await Api.Instance.SetProviderAsync(new LocalProvider()); + +// registering a provider to a domain +await Api.Instance.SetProviderAsync("clientForCache", new CachedProvider()); + +// a client backed by default provider +FeatureClient clientDefault = Api.Instance.GetClient(); + +// a client backed by CachedProvider +FeatureClient scopedClient = Api.Instance.GetClient("clientForCache"); +``` + +Domains can be defined on a provider during registration. +For more details, please refer to the [providers](#providers) section. + +### Eventing + +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, +provider readiness, or error conditions. +Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. +Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. + +Please refer to the documentation of the provider you're using to see what events are supported. + +Example usage of an Event handler: + +```csharp +public static void EventHandler(ProviderEventPayload eventDetails) +{ + Console.WriteLine(eventDetails.Type); +} +``` + +```csharp +EventHandlerDelegate callback = EventHandler; +// add an implementation of the EventHandlerDelegate for the PROVIDER_READY event +Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +It is also possible to register an event handler for a specific client, as in the following example: + +```csharp +EventHandlerDelegate callback = EventHandler; + +var myClient = Api.Instance.GetClient("my-client"); + +var provider = new ExampleProvider(); +await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider); + +myClient.AddHandler(ProviderEventTypes.ProviderReady, callback); +``` + +### Shutdown + +The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down. + +```csharp +// Shut down all providers +await Api.Instance.ShutdownAsync(); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider +{ + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a boolean flag value + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a string flag value + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve a double flag value + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + // resolve an object flag value + } +} +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. + +```csharp +public class MyHook : Hook +{ + public ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary hints = null) + { + // code to run before flag evaluation + } + + public ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary hints = null) + { + // code to run after successful flag evaluation + } + + public ValueTask ErrorAsync(HookContext context, Exception error, + IReadOnlyDictionary hints = null) + { + // code to run if there's an error during before hooks or during flag evaluation + } + + public ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary hints = null) + { + // code to run after all other stages, regardless of success/failure + } +} +``` + +Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! + + +## ⭐️ Support the project + +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more information, check out our [community page](https://openfeature.dev/community/) + +## 🤝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone who has already contributed + +[![Contrib Rocks](https://contrib.rocks/image?repo=open-feature/dotnet-sdk)](https://github.com/open-feature/dotnet-sdk/graphs/contributors) + +Made with [contrib.rocks](https://contrib.rocks). + diff --git a/build/Common.prod.props b/build/Common.prod.props index 2431a810..656f3476 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -9,7 +9,7 @@ - 1.5.0 + 2.0.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 bc80560f..227cea21 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.5.0 +2.0.0 From 36b5f8c8f763cc5d049899bdb4971ab361f1153a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 08:25:45 +1000 Subject: [PATCH 056/123] chore(deps): update dependency microsoft.net.test.sdk to 17.11.0 (#297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [Microsoft.NET.Test.Sdk](https://togithub.com/microsoft/vstest) | `17.10.0` -> `17.11.0` | [![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.NET.Test.Sdk/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/nuget/Microsoft.NET.Test.Sdk/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/nuget/Microsoft.NET.Test.Sdk/17.10.0/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.NET.Test.Sdk/17.10.0/17.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
microsoft/vstest (Microsoft.NET.Test.Sdk) ### [`v17.11.0`](https://togithub.com/microsoft/vstest/releases/tag/v17.11.0) #### What's Changed - Add reference to the AdapterUtilities library in the spec docs. by [@​peterwald](https://togithub.com/peterwald) in [https://github.com/microsoft/vstest/pull/4958](https://togithub.com/microsoft/vstest/pull/4958) - Stack trace when localized, and new messages by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4944](https://togithub.com/microsoft/vstest/pull/4944) - Fix single quote and space in F# pretty methods by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4969](https://togithub.com/microsoft/vstest/pull/4969) - Update .NET runtimes to latest patch version by [@​Evangelink](https://togithub.com/Evangelink) in [https://github.com/microsoft/vstest/pull/4975](https://togithub.com/microsoft/vstest/pull/4975) - Update dotnetcoretests.md by [@​DickBaker](https://togithub.com/DickBaker) in [https://github.com/microsoft/vstest/pull/4977](https://togithub.com/microsoft/vstest/pull/4977) - Add list of known TestingPlatform dlls by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4983](https://togithub.com/microsoft/vstest/pull/4983) - Update framework version used for testing, and test matrix by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4970](https://togithub.com/microsoft/vstest/pull/4970) - Add output forwarding for .NET by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4988](https://togithub.com/microsoft/vstest/pull/4988) - Remove usage of pt images before decomissioning by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4994](https://togithub.com/microsoft/vstest/pull/4994) - chore: Add more details to acquistion section. by [@​voroninp](https://togithub.com/voroninp) in [https://github.com/microsoft/vstest/pull/4999](https://togithub.com/microsoft/vstest/pull/4999) - Simplify banner by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5013](https://togithub.com/microsoft/vstest/pull/5013) - Forward standard output of testhost by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/4998](https://togithub.com/microsoft/vstest/pull/4998) - Add missing copyright header by [@​MichaelSimons](https://togithub.com/MichaelSimons) in [https://github.com/microsoft/vstest/pull/5020](https://togithub.com/microsoft/vstest/pull/5020) - Add option to not share .NET Framework testhosts by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5018](https://togithub.com/microsoft/vstest/pull/5018) - GetTypesToLoad Attribute cant be null by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5054](https://togithub.com/microsoft/vstest/pull/5054) - rawArgument in GetArgumentList cant be null by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5056](https://togithub.com/microsoft/vstest/pull/5056) - fix Atribute typo by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5057](https://togithub.com/microsoft/vstest/pull/5057) - remove unnecessary list alloc for 2 scenarios in TestRequestManager.GetSources by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5058](https://togithub.com/microsoft/vstest/pull/5058) - fix incompatiblity typo by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5059](https://togithub.com/microsoft/vstest/pull/5059) - remove redundant inline method in IsPlatformIncompatible by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5060](https://togithub.com/microsoft/vstest/pull/5060) - fix Sucess typo by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5061](https://togithub.com/microsoft/vstest/pull/5061) - use some null coalescing by [@​SimonCropp](https://togithub.com/SimonCropp) in [https://github.com/microsoft/vstest/pull/5062](https://togithub.com/microsoft/vstest/pull/5062) - Add cts into friends of TranslationLayer by [@​jakubch1](https://togithub.com/jakubch1) in [https://github.com/microsoft/vstest/pull/5075](https://togithub.com/microsoft/vstest/pull/5075) - Use built in sha1 for id generation by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5081](https://togithub.com/microsoft/vstest/pull/5081) - All output in terminal logger by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5083](https://togithub.com/microsoft/vstest/pull/5083) - Ignore env test by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5095](https://togithub.com/microsoft/vstest/pull/5095) - Dispose XmlReader in XmlRunSettingsUtilities by [@​omajid](https://togithub.com/omajid) in [https://github.com/microsoft/vstest/pull/5094](https://togithub.com/microsoft/vstest/pull/5094) - Bump to macos-12 build image by [@​akoeplinger](https://togithub.com/akoeplinger) in [https://github.com/microsoft/vstest/pull/5101](https://togithub.com/microsoft/vstest/pull/5101) - Handle ansi escape in terminal logger reporter by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5084](https://togithub.com/microsoft/vstest/pull/5084) - remove disable interactive auth by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5110](https://togithub.com/microsoft/vstest/pull/5110) - Error output as info in terminal logger by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5113](https://togithub.com/microsoft/vstest/pull/5113) - Write dll instead of target on abort, rename errors by [@​nohwnd](https://togithub.com/nohwnd) in [https://github.com/microsoft/vstest/pull/5115](https://togithub.com/microsoft/vstest/pull/5115) - - \[rel/17.11] Update dependencies from devdiv/DevDiv/vs-code-coverage by [@​dotnet-maestro](https://togithub.com/dotnet-maestro) in [https://github.com/microsoft/vstest/pull/5152](https://togithub.com/microsoft/vstest/pull/5152) #### New Contributors - [@​peterwald](https://togithub.com/peterwald) made their first contribution in [https://github.com/microsoft/vstest/pull/4958](https://togithub.com/microsoft/vstest/pull/4958) - [@​DickBaker](https://togithub.com/DickBaker) made their first contribution in [https://github.com/microsoft/vstest/pull/4977](https://togithub.com/microsoft/vstest/pull/4977) - [@​voroninp](https://togithub.com/voroninp) made their first contribution in [https://github.com/microsoft/vstest/pull/4999](https://togithub.com/microsoft/vstest/pull/4999) - [@​akoeplinger](https://togithub.com/akoeplinger) made their first contribution in [https://github.com/microsoft/vstest/pull/5101](https://togithub.com/microsoft/vstest/pull/5101) **Full Changelog**: https://github.com/microsoft/vstest/compare/v17.10.0...v17.11.0-release-24352-06
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/open-feature/dotnet-sdk). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f8a4159c..7227000a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From c9339ec7a0273d7581eb04b63c0b9005ec60fd60 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 06:50:02 +0100 Subject: [PATCH 057/123] chore(deps): update dependency dotnet-sdk to v8.0.401 (#296) Signed-off-by: Artyom Tonoyan --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index bc3a25e7..caa8a0e4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.400" + "version": "8.0.401" } } From 980d0bece5a6c893a9b6bf0bb978e566b9e891ea Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 24 Sep 2024 16:17:35 -0400 Subject: [PATCH 058/123] chore: update release please config (#304) ## This PR - removes the release as configuration - removes pre-1.0 configurations Signed-off-by: Michael Beemer Signed-off-by: Artyom Tonoyan --- release-please-config.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index 4ccbcc43..e79a24e5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,12 +1,9 @@ { "packages": { ".": { - "release-as": "2.0.0", "release-type": "simple", "monorepo-tags": false, "include-component-in-tag": false, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [ "build/Common.prod.props", From d4a5e3de3d8486a4c7eccd410bfe4733548559c4 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:31:38 +0400 Subject: [PATCH 059/123] [SIP-123] feat: Create OpenFeature.DependencyInjection project Signed-off-by: Artyom Tonoyan --- OpenFeature.sln | 21 ++++++++++++------- .../OpenFeature.DependencyInjection.csproj | 9 ++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj diff --git a/OpenFeature.sln b/OpenFeature.sln index 6f1cce8d..7fccd63f 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -77,7 +77,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -101,21 +103,26 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} {07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj new file mode 100644 index 00000000..edd721a3 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -0,0 +1,9 @@ + + + + netstandard2.0;net6.0;net8.0;net462 + enable + enable + + + From 8387defb840cf01d421f0e5fa24c71d4d84a8d0e Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:37:43 +0400 Subject: [PATCH 060/123] chore: Update TargetFrameworks to net6.0 Signed-off-by: Artyom Tonoyan --- .../OpenFeature.DependencyInjection.csproj | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index edd721a3..4589884a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,9 +1,14 @@ - netstandard2.0;net6.0;net8.0;net462 + net6.0; enable enable + OpenFeature + + + + From 5077a1c46f5fb9b02f3bda56f7115869efa020e4 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:38:33 +0400 Subject: [PATCH 061/123] feat: Create OpenFeatureBuilder record Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilder.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs new file mode 100644 index 00000000..03cad3ca --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature; + +/// +/// Describes a backed by an . +/// +/// +public sealed record OpenFeatureBuilder(IServiceCollection Services) +{ + /// + /// Indicates whether the evaluation context has been configured. + /// This property is used to determine if specific configurations or services + /// should be initialized based on the presence of an evaluation context. + /// + internal bool IsContextConfigured { get; set; } +} From 95e8ddce1204ad8bb1a3abff36797c4729d3f66d Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:41:45 +0400 Subject: [PATCH 062/123] feat: Add OpenFeatureBuilderExtensions Signed-off-by: Artyom Tonoyan --- .../OpenFeature.DependencyInjection.csproj | 4 ++ .../OpenFeatureBuilderExtensions.cs | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 4589884a..f6af26ef 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -11,4 +11,8 @@
+ + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..54e20426 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.Model; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// This method is used to add a new context to the service collection. + /// + /// + /// the desired configuration + /// + /// the instance + /// + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + return builder.AddContext((b, _) => configure(b)); + } + + /// + /// This method is used to add a new context to the service collection. + /// + /// + /// the desired configuration + /// + /// the instance + /// + public static OpenFeatureBuilder AddContext( + this OpenFeatureBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + builder.IsContextConfigured = true; + builder.Services.TryAddTransient(provider => { + var contextBuilder = EvaluationContext.Builder(); + configure(contextBuilder, provider); + return contextBuilder.Build(); + }); + + return builder; + } +} From f15749676542cf0d18d035d402228eec716ae0d0 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:48:35 +0400 Subject: [PATCH 063/123] feat: Add IFeatureLifecycleManager interface and implementation Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 1 + .../IFeatureLifecycleManager.cs | 24 ++++++++++++ .../Internal/FeatureLifecycleManager.cs | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs create mode 100644 src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7227000a..ed54c052 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs new file mode 100644 index 00000000..2085bda4 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs @@ -0,0 +1,24 @@ +namespace OpenFeature; + +/// +/// Defines the contract for managing the lifecycle of a feature api. +/// +public interface IFeatureLifecycleManager +{ + /// + /// Ensures that the feature provider is properly initialized and ready to be used. + /// This method should handle all necessary checks, configuration, and setup required to prepare the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of initializing the feature provider. + /// Thrown when the feature provider is not registered or is in an invalid state. + ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default); + + /// + /// Gracefully shuts down the feature api, ensuring all resources are properly disposed of and any persistent state is saved. + /// This method should handle all necessary cleanup and shutdown operations for the feature provider. + /// + /// Propagates notification that operations should be canceled. + /// A Task representing the asynchronous operation of shutting down the feature provider. + ValueTask ShutdownAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs new file mode 100644 index 00000000..1b2f47c9 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature.Internal; + +[ExcludeFromCodeCoverage] +internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager +{ + private readonly Api _featureApi; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, ILogger logger) + { + _featureApi = featureApi; + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting initialization of the feature provider"); + var featureProvider = _serviceProvider.GetService(); + if (featureProvider == null) + { + throw new InvalidOperationException("Feature provider is not registered in the service collection."); + } + await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); + } + + /// + public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Shutting down the feature provider."); + await _featureApi.ShutdownAsync().ConfigureAwait(false); + } +} From 49991eca75d9cc73ca80b176c04327f65274730f Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 4 Oct 2024 21:51:20 +0400 Subject: [PATCH 064/123] feat: Add OpenFeatureServiceCollectionExtensions Signed-off-by: Artyom Tonoyan --- .../OpenFeatureServiceCollectionExtensions.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs new file mode 100644 index 00000000..17ffc7e8 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenFeature.Internal; +using OpenFeature.Model; +using System.Diagnostics.CodeAnalysis; + +namespace OpenFeature; + +/// +/// Contains extension methods for the class. +/// +[ExcludeFromCodeCoverage] +public static class OpenFeatureServiceCollectionExtensions +{ + /// + /// This method is used to add OpenFeature to the service collection. + /// OpenFeature will be registered as a singleton. + /// + /// + /// the desired configuration + /// the current instance + public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + services.TryAddSingleton(Api.Instance); + services.TryAddSingleton(); + + var builder = new OpenFeatureBuilder(services); + configure(builder); + + if (builder.IsContextConfigured) + { + services.TryAddScoped(static provider => { + var api = provider.GetRequiredService(); + var client = api.GetClient(); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + services.TryAddScoped(static provider => { + var api = provider.GetRequiredService(); + return api.GetClient(); + }); + } + + return services; + } +} From 5bc2c47fdafcd6c52d6462c35fbb2c228b596890 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 8 Oct 2024 23:16:26 +0400 Subject: [PATCH 065/123] feat: Add AddProvider extension method in OpenFeatureBuilderExtensions Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 54e20426..579b61ca 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -11,11 +11,9 @@ public static partial class OpenFeatureBuilderExtensions /// /// This method is used to add a new context to the service collection. /// - /// + /// The instance. /// the desired configuration - /// - /// the instance - /// + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -29,11 +27,9 @@ public static OpenFeatureBuilder AddContext( /// /// This method is used to add a new context to the service collection. /// - /// + /// The instance. /// the desired configuration - /// - /// the instance - /// + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -42,7 +38,8 @@ public static OpenFeatureBuilder AddContext( ArgumentNullException.ThrowIfNull(configure); builder.IsContextConfigured = true; - builder.Services.TryAddTransient(provider => { + builder.Services.TryAddTransient(provider => + { var contextBuilder = EvaluationContext.Builder(); configure(contextBuilder, provider); return contextBuilder.Build(); @@ -50,4 +47,19 @@ public static OpenFeatureBuilder AddContext( return builder; } + + /// + /// Adds a feature provider to the service collection. + /// + /// The type of the feature provider, which must inherit from . + /// The instance. + /// A factory method to create the feature provider, using the service provider. + /// The instance. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory) + where T : FeatureProvider + { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.TryAddSingleton(providerFactory); + return builder; + } } From 9ea9b0121297473125c80f537b76e01181cca185 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:35:15 +0400 Subject: [PATCH 066/123] feat: Add AddProvider extension method in OpenFeatureServiceCollectionExtensions Signed-off-by: Artyom Tonoyan --- OpenFeature.sln | 9 ++++- .../OpenFeature.DependencyInjection.csproj | 2 +- ...enFeature.DependencyInjection.Tests.csproj | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj diff --git a/OpenFeature.sln b/OpenFeature.sln index 7fccd63f..f986b777 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -79,7 +79,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -107,6 +109,10 @@ Global {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +129,7 @@ Global {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index f6af26ef..56b00cb7 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - net6.0; + net6.0;net8.0 enable enable OpenFeature diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj new file mode 100644 index 00000000..9937e1bc --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeature.DependencyInjection.Tests.csproj @@ -0,0 +1,37 @@ + + + + net6.0;net8.0 + enable + enable + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 4066b891b1abf3a1c7c94c9c8299884ee97fe28a Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:35:47 +0400 Subject: [PATCH 067/123] test: Add OpenFeatureBuilderExtensionsTests Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 1 + .../OpenFeatureBuilderExtensionsTests.cs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ed54c052..617bec39 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs new file mode 100644 index 00000000..f66cfef4 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -0,0 +1,55 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class OpenFeatureBuilderExtensionsTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = useServiceProviderDelegate ? + builder.AddContext(_ => { }) : + builder.AddContext((_, _) => { }); + + // Assert + result.Should().BeSameAs(builder, "The method should return the same builder instance."); + services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(EvaluationContext) && + serviceDescriptor.Lifetime == ServiceLifetime.Transient, + "A transient service of type EvaluationContext should be added."); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + bool delegateCalled = false; + + _ = useServiceProviderDelegate ? + builder.AddContext(_ => delegateCalled = true) : + builder.AddContext((_, _) => delegateCalled = true); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var context = serviceProvider.GetService(); + + // Assert + context.Should().NotBeNull("The EvaluationContext should be resolvable."); + delegateCalled.Should().BeTrue("The delegate should be invoked."); + } +} From 367518bba431802c31cf9ebde28aec570a4a2008 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 14:47:04 +0400 Subject: [PATCH 068/123] test: Add unit tests for AddProvider method in OpenFeatureBuilderExtensionsTests Signed-off-by: Artyom Tonoyan --- .../Mocks/NotImplementedFeatureProvider.cs | 39 +++++++++++++++++++ .../OpenFeatureBuilderExtensionsTests.cs | 38 +++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs new file mode 100644 index 00000000..a7a857dd --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs @@ -0,0 +1,39 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +public partial class OpenFeatureBuilderExtensionsTests +{ + public class NotImplementedFeatureProvider : FeatureProvider + { + public override Metadata? GetMetadata() + { + throw new NotImplementedException(); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index f66cfef4..67099f3f 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -5,7 +5,7 @@ namespace OpenFeature.DependencyInjection.Tests; -public class OpenFeatureBuilderExtensionsTests +public partial class OpenFeatureBuilderExtensionsTests { [Theory] [InlineData(true)] @@ -52,4 +52,40 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe context.Should().NotBeNull("The EvaluationContext should be resolvable."); delegateCalled.Should().BeTrue("The delegate should be invoked."); } + + [Fact] + public void AddProvider_ShouldAddProviderToCollection() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + + // Act + var result = builder.AddProvider(_ => new NotImplementedFeatureProvider()); + + // Assert + result.Should().BeSameAs(builder, "The method should return the same builder instance."); + services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(FeatureProvider) && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + "A singleton service of type FeatureProvider should be added."); + } + + [Fact] + public void AddProvider_ShouldResolveCorrectProvider() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenFeatureBuilder(services); + builder.AddProvider(_ => new NotImplementedFeatureProvider()); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetService(); + + // Assert + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } } From 70c269fd8485515d63c24defc82edcb4fe6a17c8 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:04:30 +0400 Subject: [PATCH 069/123] feat: Replicate NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider Signed-off-by: Artyom Tonoyan --- .../Mocks/NotImplementedFeatureProvider.cs | 39 -------------- .../NoOpFeatureProvider.cs | 52 +++++++++++++++++++ .../NoOpProvider.cs | 8 +++ 3 files changed, 60 insertions(+), 39 deletions(-) delete mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs deleted file mode 100644 index a7a857dd..00000000 --- a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using OpenFeature.Model; - -namespace OpenFeature.DependencyInjection.Tests; - -public partial class OpenFeatureBuilderExtensionsTests -{ - public class NotImplementedFeatureProvider : FeatureProvider - { - public override Metadata? GetMetadata() - { - throw new NotImplementedException(); - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } -} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs new file mode 100644 index 00000000..ac3e5209 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProvider.cs @@ -0,0 +1,52 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +// This class replicates the NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider.cs. +// It is used here to facilitate unit testing without relying on the internal NoOpFeatureProvider class. +// If the InternalsVisibleTo attribute is added to the OpenFeature project, +// this class can be removed and the original NoOpFeatureProvider can be directly accessed for testing. +internal sealed class NoOpFeatureProvider : FeatureProvider +{ + private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName); + + public override Metadata GetMetadata() + { + return this._metadata; + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + return Task.FromResult(NoOpResponse(flagKey, defaultValue)); + } + + private static ResolutionDetails NoOpResponse(string flagKey, T defaultValue) + { + return new ResolutionDetails( + flagKey, + defaultValue, + reason: NoOpProvider.ReasonNoOp, + variant: NoOpProvider.Variant + ); + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs new file mode 100644 index 00000000..7bf20bca --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpProvider.cs @@ -0,0 +1,8 @@ +namespace OpenFeature.DependencyInjection.Tests; + +internal static class NoOpProvider +{ + public const string NoOpProviderName = "No-op Provider"; + public const string ReasonNoOp = "No-op"; + public const string Variant = "No-op"; +} From b20bc13fb5d69bd0e8bb389e4dcced2a0aa63526 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:05:09 +0400 Subject: [PATCH 070/123] test: Update OpenFeatureBuilderExtensionsTests with NoOpFeatureProvider usage Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensionsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 67099f3f..da7742b6 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -61,7 +61,7 @@ public void AddProvider_ShouldAddProviderToCollection() var builder = new OpenFeatureBuilder(services); // Act - var result = builder.AddProvider(_ => new NotImplementedFeatureProvider()); + var result = builder.AddProvider(_ => new NoOpFeatureProvider()); // Assert result.Should().BeSameAs(builder, "The method should return the same builder instance."); @@ -77,7 +77,7 @@ public void AddProvider_ShouldResolveCorrectProvider() // Arrange var services = new ServiceCollection(); var builder = new OpenFeatureBuilder(services); - builder.AddProvider(_ => new NotImplementedFeatureProvider()); + builder.AddProvider(_ => new NoOpFeatureProvider()); var serviceProvider = services.BuildServiceProvider(); @@ -86,6 +86,6 @@ public void AddProvider_ShouldResolveCorrectProvider() // Assert provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } } From c251f566279ae6e5253d1fcd5050679bcbd1aa9f Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:05:36 +0400 Subject: [PATCH 071/123] test: Add OpenFeatureBuilderExtensionsTests Signed-off-by: Artyom Tonoyan --- .../OpenFeature.DependencyInjection.csproj | 9 +++ .../FeatureLifecycleManagerTests.cs | 57 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 56b00cb7..775d4d6d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -15,4 +15,13 @@ + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs new file mode 100644 index 00000000..8b9823e0 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using OpenFeature.Internal; +using Xunit; + +namespace OpenFeature.DependencyInjection.Tests; + +public class FeatureLifecycleManagerTests +{ + private readonly FeatureLifecycleManager _systemUnderTest; + private readonly IServiceProvider _mockServiceProvider; + + public FeatureLifecycleManagerTests() + { + Api.Instance.SetContext(null); + Api.Instance.ClearHooks(); + + //_mockApi = Substitute.ForPartsOf(); + //Api.Instance.Returns(_mockApi); + + _mockServiceProvider = Substitute.For(); + + _systemUnderTest = new FeatureLifecycleManager( + Api.Instance, + _mockServiceProvider, + Substitute.For>()); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExists() + { + // Arrange + var featureProvider = new NoOpFeatureProvider(); + _mockServiceProvider.GetService(typeof(FeatureProvider)) + .Returns(featureProvider); + + // Act + await _systemUnderTest.EnsureInitializedAsync(); + + // Assert + Api.Instance.GetProvider().Should().BeSameAs(featureProvider); + } + + [Fact] + public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist() + { + // Arrange + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(false)); + + exception.Message.Should().Be("Feature provider is not registered in the service collection."); + } +} From 8b2ab4bac91f81f1c4a397b02cc168bfcb604262 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:06:42 +0400 Subject: [PATCH 072/123] refactor: Remove ExcludeFromCodeCoverage attribute from FeatureLifecycleManager Signed-off-by: Artyom Tonoyan --- .../Internal/FeatureLifecycleManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 1b2f47c9..4b6ad426 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature.Internal; -[ExcludeFromCodeCoverage] internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager { private readonly Api _featureApi; From 3800447b69c2f874e63244a0b5cadcbaf82a7ac3 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:10:58 +0400 Subject: [PATCH 073/123] test: Improve OpenFeatureBuilderExtensionsTests for better coverage and clarity Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensionsTests.cs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index da7742b6..d4726db5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -7,23 +7,29 @@ namespace OpenFeature.DependencyInjection.Tests; public partial class OpenFeatureBuilderExtensionsTests { + private readonly IServiceCollection _services; + private readonly OpenFeatureBuilder _systemUnderTest; + + public OpenFeatureBuilderExtensionsTests() + { + _services = new ServiceCollection(); + _systemUnderTest = new OpenFeatureBuilder(_services); + } + [Theory] [InlineData(true)] [InlineData(false)] public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProviderDelegate) { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - // Act var result = useServiceProviderDelegate ? - builder.AddContext(_ => { }) : - builder.AddContext((_, _) => { }); + _systemUnderTest.AddContext(_ => { }) : + _systemUnderTest.AddContext((_, _) => { }); // Assert - result.Should().BeSameAs(builder, "The method should return the same builder instance."); - services.Should().ContainSingle(serviceDescriptor => + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); + _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(EvaluationContext) && serviceDescriptor.Lifetime == ServiceLifetime.Transient, "A transient service of type EvaluationContext should be added."); @@ -35,20 +41,19 @@ public void AddContext_Delegate_ShouldAddServiceToCollection(bool useServiceProv public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDelegate) { // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); bool delegateCalled = false; _ = useServiceProviderDelegate ? - builder.AddContext(_ => delegateCalled = true) : - builder.AddContext((_, _) => delegateCalled = true); + _systemUnderTest.AddContext(_ => delegateCalled = true) : + _systemUnderTest.AddContext((_, _) => delegateCalled = true); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = _services.BuildServiceProvider(); // Act var context = serviceProvider.GetService(); // Assert + _systemUnderTest.IsContextConfigured.Should().BeTrue("The context should be configured."); context.Should().NotBeNull("The EvaluationContext should be resolvable."); delegateCalled.Should().BeTrue("The delegate should be invoked."); } @@ -56,16 +61,13 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe [Fact] public void AddProvider_ShouldAddProviderToCollection() { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - // Act - var result = builder.AddProvider(_ => new NoOpFeatureProvider()); + var result = _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); // Assert - result.Should().BeSameAs(builder, "The method should return the same builder instance."); - services.Should().ContainSingle(serviceDescriptor => + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _services.Should().ContainSingle(serviceDescriptor => serviceDescriptor.ServiceType == typeof(FeatureProvider) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton, "A singleton service of type FeatureProvider should be added."); @@ -75,16 +77,15 @@ public void AddProvider_ShouldAddProviderToCollection() public void AddProvider_ShouldResolveCorrectProvider() { // Arrange - var services = new ServiceCollection(); - var builder = new OpenFeatureBuilder(services); - builder.AddProvider(_ => new NoOpFeatureProvider()); + _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = _services.BuildServiceProvider(); // Act var provider = serviceProvider.GetService(); // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); provider.Should().NotBeNull("The FeatureProvider should be resolvable."); provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); } From e111714790cfdff4fc7371f5cce2a32fcbdb5fa0 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:11:53 +0400 Subject: [PATCH 074/123] test: Create OpenFeatureServiceCollectionExtensionsTests Signed-off-by: Artyom Tonoyan --- .../OpenFeatureServiceCollectionExtensions.cs | 2 - ...FeatureServiceCollectionExtensionsTests.cs | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 17ffc7e8..88061f55 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -2,14 +2,12 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using OpenFeature.Internal; using OpenFeature.Model; -using System.Diagnostics.CodeAnalysis; namespace OpenFeature; /// /// Contains extension methods for the class. /// -[ExcludeFromCodeCoverage] public static class OpenFeatureServiceCollectionExtensions { /// diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..4149b8a8 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace OpenFeature.Tests; + +public class OpenFeatureServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _systemUnderTest; + private readonly Action _configureAction; + + public OpenFeatureServiceCollectionExtensionsTests() + { + _systemUnderTest = new ServiceCollection(); + _configureAction = Substitute.For>(); + } + + [Fact] + public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSingleton() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + _systemUnderTest.Should().HaveCount(3); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); + _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); + } + + [Fact] + public void AddOpenFeature_ShouldInvokeConfigureAction() + { + // Act + _systemUnderTest.AddOpenFeature(_configureAction); + + // Assert + _configureAction.Received(1).Invoke(Arg.Any()); + } +} From cb63b1cefba555295767a7a7505832df7ada1b09 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 20:35:49 +0400 Subject: [PATCH 075/123] feat: Create OpenFeature.Hosting project Signed-off-by: Artyom Tonoyan --- Directory.Packages.props | 11 ++++++----- OpenFeature.sln | 9 ++++++++- src/OpenFeature.Hosting/OpenFeature.Hosting.csproj | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 src/OpenFeature.Hosting/OpenFeature.Hosting.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 617bec39..8e4347e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,18 +1,19 @@ - + true - + + - + @@ -29,9 +30,9 @@ - + - + diff --git a/OpenFeature.sln b/OpenFeature.sln index f986b777..5ce227c5 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -81,7 +81,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -113,6 +115,10 @@ Global {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +136,7 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj new file mode 100644 index 00000000..30771b45 --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -0,0 +1,14 @@ + + + + net6.0;net8.0 + enable + enable + OpenFeature + + + + + + + From 0e53494af799182a2abebd16295c8ada16df7074 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 22:50:49 +0400 Subject: [PATCH 076/123] feat: Add HostedFeatureLifecycleService and related logic Signed-off-by: Artyom Tonoyan --- .../FeatureLifecycleStateOptions.cs | 18 ++++ src/OpenFeature.Hosting/FeatureStartState.cs | 22 +++++ src/OpenFeature.Hosting/FeatureStopState.cs | 22 +++++ .../HostedFeatureLifecycleService.cs | 93 +++++++++++++++++++ .../OpenFeature.Hosting.csproj | 4 + .../OpenFeatureBuilderExtensions.cs | 35 +++++++ 6 files changed, 194 insertions(+) create mode 100644 src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs create mode 100644 src/OpenFeature.Hosting/FeatureStartState.cs create mode 100644 src/OpenFeature.Hosting/FeatureStopState.cs create mode 100644 src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs create mode 100644 src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs diff --git a/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs new file mode 100644 index 00000000..91e3047d --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureLifecycleStateOptions.cs @@ -0,0 +1,18 @@ +namespace OpenFeature; + +/// +/// Represents the lifecycle state options for a feature, +/// defining the states during the start and stop lifecycle. +/// +public class FeatureLifecycleStateOptions +{ + /// + /// Gets or sets the state during the feature startup lifecycle. + /// + public FeatureStartState StartState { get; set; } = FeatureStartState.Starting; + + /// + /// Gets or sets the state during the feature shutdown lifecycle. + /// + public FeatureStopState StopState { get; set; } = FeatureStopState.Stopping; +} diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs new file mode 100644 index 00000000..3aa59179 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStartState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for starting a feature. +/// +public enum FeatureStartState +{ + /// + /// The feature is in the process of starting. + /// + Starting, + + /// + /// The feature is at the start state. + /// + Start, + + /// + /// The feature has fully started. + /// + Started +} diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs new file mode 100644 index 00000000..a8298da0 --- /dev/null +++ b/src/OpenFeature.Hosting/FeatureStopState.cs @@ -0,0 +1,22 @@ +namespace OpenFeature; + +/// +/// Defines the various states for stopping a feature. +/// +public enum FeatureStopState +{ + /// + /// The feature is in the process of stopping. + /// + Stopping, + + /// + /// The feature is at the stop state. + /// + Stop, + + /// + /// The feature has fully stopped. + /// + Stopped +} diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs new file mode 100644 index 00000000..1e3b3c30 --- /dev/null +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenFeature; + +/// +/// A hosted service that manages the lifecycle of features within the application. +/// It ensures that features are properly initialized when the service starts +/// and gracefully shuts down when the service stops. +/// +public sealed class HostedFeatureLifecycleService : IHostedLifecycleService +{ + private readonly ILogger _logger; + private readonly IFeatureLifecycleManager _featureLifecycleManager; + private readonly IOptions _featureLifecycleStateOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The logger used to log lifecycle events. + /// The feature lifecycle manager responsible for initialization and shutdown. + /// Options that define the start and stop states of the feature lifecycle. + public HostedFeatureLifecycleService( + ILogger logger, + IFeatureLifecycleManager featureLifecycleManager, + IOptions featureLifecycleStateOptions) + { + _logger = logger; + _featureLifecycleManager = featureLifecycleManager; + _featureLifecycleStateOptions = featureLifecycleStateOptions; + } + + /// + /// Ensures that the feature is properly initialized when the service starts. + /// + public async Task StartingAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Starting, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Start" state. + /// + public async Task StartAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Start, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully started and operational. + /// + public async Task StartedAsync(CancellationToken cancellationToken) + => await InitializeIfStateMatchesAsync(FeatureStartState.Started, cancellationToken).ConfigureAwait(false); + + /// + /// Gracefully shuts down the feature when the service is stopping. + /// + public async Task StoppingAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopping, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is in the "Stop" state. + /// + public async Task StopAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stop, cancellationToken).ConfigureAwait(false); + + /// + /// Ensures that the feature is fully stopped and no longer operational. + /// + public async Task StoppedAsync(CancellationToken cancellationToken) + => await ShutdownIfStateMatchesAsync(FeatureStopState.Stopped, cancellationToken).ConfigureAwait(false); + + /// + /// Initializes the feature lifecycle if the current state matches the expected start state. + /// + private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StartState == expectedState) + { + _logger.LogInformation("Initializing the Feature Lifecycle Manager for state {State}.", expectedState); + await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Shuts down the feature lifecycle if the current state matches the expected stop state. + /// + private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, CancellationToken cancellationToken) + { + if (_featureLifecycleStateOptions.Value.StopState == expectedState) + { + _logger.LogInformation("Shutting down the Feature Lifecycle Manager for state {State}.", expectedState); + await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj index 30771b45..48730084 100644 --- a/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj +++ b/src/OpenFeature.Hosting/OpenFeature.Hosting.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs new file mode 100644 index 00000000..d45efd1c --- /dev/null +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OpenFeature; + +/// +/// Extension methods for configuring the hosted feature lifecycle in the . +/// +public static partial class OpenFeatureBuilderExtensions +{ + /// + /// Adds the to the OpenFeatureBuilder, + /// which manages the lifecycle of features within the application. It also allows + /// configuration of the . + /// + /// The instance. + /// An optional action to configure . + /// The instance. + public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) + { + if(configureOptions == null) + { + builder.Services.Configure(cfg => { + cfg.StartState = FeatureStartState.Starting; + cfg.StopState = FeatureStopState.Stopping; + }); + } + else + { + builder.Services.Configure(configureOptions); + } + + builder.Services.AddHostedService(); + return builder; + } +} From dc765e690765f143e28e37aff25d18eaeb712111 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 14 Oct 2024 08:53:43 +0400 Subject: [PATCH 077/123] test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist test Signed-off-by: Artyom Tonoyan --- .../FeatureLifecycleManagerTests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 8b9823e0..903be3cf 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -16,9 +16,6 @@ public FeatureLifecycleManagerTests() Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - //_mockApi = Substitute.ForPartsOf(); - //Api.Instance.Returns(_mockApi); - _mockServiceProvider = Substitute.For(); _systemUnderTest = new FeatureLifecycleManager( @@ -48,10 +45,11 @@ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNo // Arrange _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(null as FeatureProvider); - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(false)); + // Act + var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); + // Assert + var exception = await Assert.ThrowsAsync(act); exception.Message.Should().Be("Feature provider is not registered in the service collection."); } } From 4f80593df615d026bd0a94d8e5d0c101626c826d Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:06:26 +0400 Subject: [PATCH 078/123] add-dependency-injection --- OpenFeature.sln | 27 +++++-------------- .../OpenFeature.DependencyInjection.csproj | 2 +- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/OpenFeature.sln b/OpenFeature.sln index 5ce227c5..28c968d6 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -79,11 +79,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -107,18 +103,6 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU - {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU - {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU - {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,9 +118,12 @@ Global {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} - {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} + {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 775d4d6d..c86e5ed9 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + netstandard2.0;net6.0;net8.0;net462 enable enable OpenFeature From 0a76193501638a18612b643c99fe91bba5ca09a4 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:07:43 +0400 Subject: [PATCH 079/123] add-dependency-injection --- .../OpenFeature.DependencyInjection.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index c86e5ed9..48a001ef 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0;net8.0;net462 + net6.0; enable enable OpenFeature From ac98ddd4e680683dfcd90a8d71d93ffcfee5f7ee Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:10:18 +0400 Subject: [PATCH 080/123] add-dependency-injection --- .../OpenFeatureBuilderExtensions.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 579b61ca..1ba827ef 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -27,9 +27,7 @@ public static OpenFeatureBuilder AddContext( /// /// This method is used to add a new context to the service collection. /// - /// The instance. /// the desired configuration - /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -38,8 +36,6 @@ public static OpenFeatureBuilder AddContext( ArgumentNullException.ThrowIfNull(configure); builder.IsContextConfigured = true; - builder.Services.TryAddTransient(provider => - { var contextBuilder = EvaluationContext.Builder(); configure(contextBuilder, provider); return contextBuilder.Build(); From 4af39cef698f4f46e779c8c04d0f44d063b1c4d2 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:15:50 +0400 Subject: [PATCH 081/123] add-dependency-injection --- .../OpenFeatureBuilderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 1ba827ef..3ff6daf0 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -27,7 +27,9 @@ public static OpenFeatureBuilder AddContext( /// /// This method is used to add a new context to the service collection. /// + /// The instance. /// the desired configuration + /// The instance. public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) @@ -36,6 +38,7 @@ public static OpenFeatureBuilder AddContext( ArgumentNullException.ThrowIfNull(configure); builder.IsContextConfigured = true; + builder.Services.TryAddTransient(provider => { var contextBuilder = EvaluationContext.Builder(); configure(contextBuilder, provider); return contextBuilder.Build(); From 0fa6dc331108ef3136c13e8800c966eafb71ef9d Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:17:48 +0400 Subject: [PATCH 082/123] add-dependency-injection --- OpenFeature.sln | 19 ++++++++++++------- .../OpenFeature.DependencyInjection.csproj | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/OpenFeature.sln b/OpenFeature.sln index 28c968d6..712d0e88 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -79,7 +79,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -103,6 +105,14 @@ Global {7398C446-2630-4F8C-9278-4E807720E9E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {7398C446-2630-4F8C-9278-4E807720E9E5}.Release|Any CPU.Build.0 = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5415057-2700-48B5-940A-7A10969FA639}.Release|Any CPU.Build.0 = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -118,12 +128,7 @@ Global {49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} - {C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {09BAB3A2-E94C-490A-861C-7D1E11BB7024} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} - {72005F60-C2E8-40BF-AE95-893635134D7D} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {9392E03B-4E6B-434C-8553-B859424388B1} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8} = {E8916D4F-B97E-42D6-8620-ED410A106F94} - {4BB69DB3-9653-4197-9589-37FA6D658CB7} = {E8916D4F-B97E-42D6-8620-ED410A106F94} + {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 48a001ef..775d4d6d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - net6.0; + net6.0;net8.0 enable enable OpenFeature From 926c0d4e7e1a7d423c659bb155989c4af3f86d3e Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:21:29 +0400 Subject: [PATCH 083/123] add-dependency-injection --- .../OpenFeatureBuilderExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index d4726db5..d4e6fc3d 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -5,7 +5,7 @@ namespace OpenFeature.DependencyInjection.Tests; -public partial class OpenFeatureBuilderExtensionsTests +public class OpenFeatureBuilderExtensionsTests { private readonly IServiceCollection _services; private readonly OpenFeatureBuilder _systemUnderTest; From 9b8d9f21da83c7047eaf039d0038e402c7289168 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:22:44 +0400 Subject: [PATCH 084/123] add-dependency-injection --- .../Mocks/NotImplementedFeatureProvider.cs | 39 +++++++++++++++++++ .../OpenFeatureBuilderExtensionsTests.cs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs new file mode 100644 index 00000000..a7a857dd --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs @@ -0,0 +1,39 @@ +using OpenFeature.Model; + +namespace OpenFeature.DependencyInjection.Tests; + +public partial class OpenFeatureBuilderExtensionsTests +{ + public class NotImplementedFeatureProvider : FeatureProvider + { + public override Metadata? GetMetadata() + { + throw new NotImplementedException(); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index d4e6fc3d..d4726db5 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -5,7 +5,7 @@ namespace OpenFeature.DependencyInjection.Tests; -public class OpenFeatureBuilderExtensionsTests +public partial class OpenFeatureBuilderExtensionsTests { private readonly IServiceCollection _services; private readonly OpenFeatureBuilder _systemUnderTest; From aa488aecaf32e57ad1451e88fc7e83e9473599da Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 13 Oct 2024 19:04:30 +0400 Subject: [PATCH 085/123] feat: Replicate NoOpFeatureProvider implementation from src/OpenFeature/NoOpFeatureProvider Signed-off-by: Artyom Tonoyan --- .../Mocks/NotImplementedFeatureProvider.cs | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs b/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs deleted file mode 100644 index a7a857dd..00000000 --- a/test/OpenFeature.DependencyInjection.Tests/Mocks/NotImplementedFeatureProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using OpenFeature.Model; - -namespace OpenFeature.DependencyInjection.Tests; - -public partial class OpenFeatureBuilderExtensionsTests -{ - public class NotImplementedFeatureProvider : FeatureProvider - { - public override Metadata? GetMetadata() - { - throw new NotImplementedException(); - } - - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } -} From 1f8e9e4cea73b44cfdd94a6a1fa5b30074ebe0ad Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:24:11 +0400 Subject: [PATCH 086/123] add-dependency-injection --- .../FeatureLifecycleManagerTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 903be3cf..1f95a129 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -16,6 +16,9 @@ public FeatureLifecycleManagerTests() Api.Instance.SetContext(null); Api.Instance.ClearHooks(); + //_mockApi = Substitute.ForPartsOf(); + //Api.Instance.Returns(_mockApi); + _mockServiceProvider = Substitute.For(); _systemUnderTest = new FeatureLifecycleManager( From 3aaa7df2ef6169cd646858f1f4cff0cc158a906c Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 14 Oct 2024 08:53:43 +0400 Subject: [PATCH 087/123] test: Improve EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNotExist test Signed-off-by: Artyom Tonoyan --- .../FeatureLifecycleManagerTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 1f95a129..903be3cf 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -16,9 +16,6 @@ public FeatureLifecycleManagerTests() Api.Instance.SetContext(null); Api.Instance.ClearHooks(); - //_mockApi = Substitute.ForPartsOf(); - //Api.Instance.Returns(_mockApi); - _mockServiceProvider = Substitute.For(); _systemUnderTest = new FeatureLifecycleManager( From ffef1acf58d9a5f669ee3d10d1b0aeb9197c99fb Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:26:57 +0400 Subject: [PATCH 088/123] add-dependency-injection --- OpenFeature.sln | 2 ++ .../CallerArgumentExpressionAttribute.cs | 23 +++++++++++++++++++ .../MultiTarget/Guard.cs | 20 ++++++++++++++++ .../MultiTarget/IsExternalInit.cs | 21 +++++++++++++++++ .../OpenFeature.DependencyInjection.csproj | 8 +++++-- .../OpenFeatureBuilder.cs | 2 +- .../OpenFeatureBuilderExtensions.cs | 10 ++++---- .../OpenFeatureServiceCollectionExtensions.cs | 4 ++-- 8 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs create mode 100644 src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs diff --git a/OpenFeature.sln b/OpenFeature.sln index 712d0e88..74ee2513 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -83,6 +83,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjec EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..afbec6b0 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,23 @@ +// @formatter:off +// ReSharper disable All +#if NETCOREAPP3_0_OR_GREATER +// https://github.com/dotnet/runtime/issues/96197 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))] +#else +#pragma warning disable +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } +} +#endif diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs new file mode 100644 index 00000000..e086fc23 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace OpenFeature; + +[DebuggerStepThrough] +internal static class Guard +{ + public static T ThrowIfNull(T? value, [CallerArgumentExpression("value")] string name = null!) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value, name); +#else + if (value is null) + throw new ArgumentNullException(name); +#endif + + return value; + } +} diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs new file mode 100644 index 00000000..87714111 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/MultiTarget/IsExternalInit.cs @@ -0,0 +1,21 @@ +// @formatter:off +// ReSharper disable All +#if NET5_0_OR_GREATER +// https://github.com/dotnet/runtime/issues/96197 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] +#else +#pragma warning disable +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +static class IsExternalInit { } +#endif diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 775d4d6d..fcdfd6ad 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,7 +1,7 @@ - + - net6.0;net8.0 + netstandard2.0;net6.0;net8.0;net462 enable enable OpenFeature @@ -24,4 +24,8 @@ + + + + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index 03cad3ca..3ca3c10d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -5,7 +5,7 @@ namespace OpenFeature; /// /// Describes a backed by an . /// -/// +/// The instance. public sealed record OpenFeatureBuilder(IServiceCollection Services) { /// diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 3ff6daf0..47f474c7 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -18,8 +18,8 @@ public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configure); + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); return builder.AddContext((b, _) => configure(b)); } @@ -34,8 +34,8 @@ public static OpenFeatureBuilder AddContext( this OpenFeatureBuilder builder, Action configure) { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(configure); + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); builder.IsContextConfigured = true; builder.Services.TryAddTransient(provider => { @@ -57,7 +57,7 @@ public static OpenFeatureBuilder AddContext( public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory) where T : FeatureProvider { - ArgumentNullException.ThrowIfNull(builder); + Guard.ThrowIfNull(builder); builder.Services.TryAddSingleton(providerFactory); return builder; } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 88061f55..863527af 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -19,8 +19,8 @@ public static class OpenFeatureServiceCollectionExtensions /// the current instance public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configure); + Guard.ThrowIfNull(services); + Guard.ThrowIfNull(configure); services.TryAddSingleton(Api.Instance); services.TryAddSingleton(); From 1acb4fa1e73008eac9bb362fb0e2415836f363ce Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 16 Oct 2024 14:28:14 +0400 Subject: [PATCH 089/123] add-dependency-injection --- OpenFeature.sln | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/OpenFeature.sln b/OpenFeature.sln index 74ee2513..e8191acd 100644 --- a/OpenFeature.sln +++ b/OpenFeature.sln @@ -81,9 +81,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection", "src\OpenFeature.DependencyInjection\OpenFeature.DependencyInjection.csproj", "{C5415057-2700-48B5-940A-7A10969FA639}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.DependencyInjection.Tests", "test\OpenFeature.DependencyInjection.Tests\OpenFeature.DependencyInjection.Tests.csproj", "{EB35F9F6-8A79-410E-A293-9387BC4AC9A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Hosting", "src\OpenFeature.Hosting\OpenFeature.Hosting.csproj", "{C99DA02A-3981-45A6-B3F8-4A1A48653DEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -115,6 +115,10 @@ Global {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB35F9F6-8A79-410E-A293-9387BC4AC9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -131,6 +135,8 @@ Global {90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} {C5415057-2700-48B5-940A-7A10969FA639} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} + {EB35F9F6-8A79-410E-A293-9387BC4AC9A7} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9} + {C99DA02A-3981-45A6-B3F8-4A1A48653DEE} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F} From 54c145e05ea4adcefb765eaf7e808a6db69735b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:01:30 +0100 Subject: [PATCH 090/123] Fix dotnet format build. 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> --- .../OpenFeatureBuilderExtensions.cs | 3 ++- .../OpenFeatureServiceCollectionExtensions.cs | 6 ++++-- src/OpenFeature.Hosting/FeatureStartState.cs | 2 +- src/OpenFeature.Hosting/FeatureStopState.cs | 2 +- src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs | 5 +++-- .../FeatureLifecycleManagerTests.cs | 4 ++-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 47f474c7..8cc8e07a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -38,7 +38,8 @@ public static OpenFeatureBuilder AddContext( Guard.ThrowIfNull(configure); builder.IsContextConfigured = true; - builder.Services.TryAddTransient(provider => { + builder.Services.TryAddTransient(provider => + { var contextBuilder = EvaluationContext.Builder(); configure(contextBuilder, provider); return contextBuilder.Build(); diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 863527af..28fb000f 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -30,7 +30,8 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services if (builder.IsContextConfigured) { - services.TryAddScoped(static provider => { + services.TryAddScoped(static provider => + { var api = provider.GetRequiredService(); var client = api.GetClient(); var context = provider.GetRequiredService(); @@ -40,7 +41,8 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services } else { - services.TryAddScoped(static provider => { + services.TryAddScoped(static provider => + { var api = provider.GetRequiredService(); return api.GetClient(); }); diff --git a/src/OpenFeature.Hosting/FeatureStartState.cs b/src/OpenFeature.Hosting/FeatureStartState.cs index 3aa59179..8001b9c2 100644 --- a/src/OpenFeature.Hosting/FeatureStartState.cs +++ b/src/OpenFeature.Hosting/FeatureStartState.cs @@ -1,4 +1,4 @@ -namespace OpenFeature; +namespace OpenFeature; /// /// Defines the various states for starting a feature. diff --git a/src/OpenFeature.Hosting/FeatureStopState.cs b/src/OpenFeature.Hosting/FeatureStopState.cs index a8298da0..d8d6a28c 100644 --- a/src/OpenFeature.Hosting/FeatureStopState.cs +++ b/src/OpenFeature.Hosting/FeatureStopState.cs @@ -1,4 +1,4 @@ -namespace OpenFeature; +namespace OpenFeature; /// /// Defines the various states for stopping a feature. diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs index d45efd1c..384d0471 100644 --- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -17,9 +17,10 @@ public static partial class OpenFeatureBuilderExtensions /// The instance. public static OpenFeatureBuilder AddHostedFeatureLifecycle(this OpenFeatureBuilder builder, Action? configureOptions = null) { - if(configureOptions == null) + if (configureOptions == null) { - builder.Services.Configure(cfg => { + builder.Services.Configure(cfg => + { cfg.StartState = FeatureStartState.Starting; cfg.StopState = FeatureStopState.Stopping; }); diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 903be3cf..c18495d3 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -33,7 +33,7 @@ public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExi .Returns(featureProvider); // Act - await _systemUnderTest.EnsureInitializedAsync(); + await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true); // Assert Api.Instance.GetProvider().Should().BeSameAs(featureProvider); @@ -49,7 +49,7 @@ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNo var act = () => _systemUnderTest.EnsureInitializedAsync().AsTask(); // Assert - var exception = await Assert.ThrowsAsync(act); + var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true); exception.Message.Should().Be("Feature provider is not registered in the service collection."); } } From 55582d72d70543e1a6b35bae4f2fd61a1ce93593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:52:27 +0100 Subject: [PATCH 091/123] Changing to Source Generation logging. 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> --- .../Internal/FeatureLifecycleManager.cs | 12 +++++++++--- .../HostedFeatureLifecycleService.cs | 16 +++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 4b6ad426..9472b4f9 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -3,7 +3,7 @@ namespace OpenFeature.Internal; -internal sealed class FeatureLifecycleManager : IFeatureLifecycleManager +internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager { private readonly Api _featureApi; private readonly IServiceProvider _serviceProvider; @@ -19,7 +19,7 @@ public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, /// public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) { - _logger.LogInformation("Starting initialization of the feature provider"); + this.LogStartingInitializationOfFeatureProvider(); var featureProvider = _serviceProvider.GetService(); if (featureProvider == null) { @@ -31,7 +31,13 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke /// public async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) { - _logger.LogInformation("Shutting down the feature provider."); + this.LogShuttingDownFeatureProvider(); await _featureApi.ShutdownAsync().ConfigureAwait(false); } + + [LoggerMessage(200, LogLevel.Information, "Starting initialization of the feature provider")] + partial void LogStartingInitializationOfFeatureProvider(); + + [LoggerMessage(200, LogLevel.Information, "Shutting down the feature provider")] + partial void LogShuttingDownFeatureProvider(); } diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs index 1e3b3c30..dd9bcbee 100644 --- a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -5,11 +5,11 @@ namespace OpenFeature; /// -/// A hosted service that manages the lifecycle of features within the application. -/// It ensures that features are properly initialized when the service starts +/// A hosted service that manages the lifecycle of features within the application. +/// It ensures that features are properly initialized when the service starts /// and gracefully shuts down when the service stops. /// -public sealed class HostedFeatureLifecycleService : IHostedLifecycleService +public sealed partial class HostedFeatureLifecycleService : IHostedLifecycleService { private readonly ILogger _logger; private readonly IFeatureLifecycleManager _featureLifecycleManager; @@ -74,7 +74,7 @@ private async Task InitializeIfStateMatchesAsync(FeatureStartState expectedState { if (_featureLifecycleStateOptions.Value.StartState == expectedState) { - _logger.LogInformation("Initializing the Feature Lifecycle Manager for state {State}.", expectedState); + this.LogInitializingFeatureLifecycleManager(expectedState); await _featureLifecycleManager.EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); } } @@ -86,8 +86,14 @@ private async Task ShutdownIfStateMatchesAsync(FeatureStopState expectedState, C { if (_featureLifecycleStateOptions.Value.StopState == expectedState) { - _logger.LogInformation("Shutting down the Feature Lifecycle Manager for state {State}.", expectedState); + this.LogShuttingDownFeatureLifecycleManager(expectedState); await _featureLifecycleManager.ShutdownAsync(cancellationToken).ConfigureAwait(false); } } + + [LoggerMessage(200, LogLevel.Information, "Initializing the Feature Lifecycle Manager for state {State}.")] + partial void LogInitializingFeatureLifecycleManager(FeatureStartState state); + + [LoggerMessage(200, LogLevel.Information, "Shutting down the Feature Lifecycle Manager for state {State}")] + partial void LogShuttingDownFeatureLifecycleManager(FeatureStopState state); } From fe5e072365063167e92baca2ff5d5f984ae4f6a0 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 21 Oct 2024 10:33:35 +0400 Subject: [PATCH 092/123] refactor: Update ThrowIfNull method --- .../MultiTarget/Guard.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs index e086fc23..8d2726b9 100644 --- a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs +++ b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs @@ -6,15 +6,9 @@ namespace OpenFeature; [DebuggerStepThrough] internal static class Guard { - public static T ThrowIfNull(T? value, [CallerArgumentExpression("value")] string name = null!) + public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { -#if NET8_0_OR_GREATER - ArgumentNullException.ThrowIfNull(value, name); -#else - if (value is null) - throw new ArgumentNullException(name); -#endif - - return value; + if (argument is null) + throw new ArgumentNullException(paramName); } } From 10ea9f11b6eb68e4e2ec262b7daa4692d9cfcc61 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 23 Oct 2024 01:29:14 +0400 Subject: [PATCH 093/123] refactor: Change OpenFeatureBuilder to class and make IsContextConfigured public --- .../OpenFeatureBuilder.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index 3ca3c10d..6d55dcd0 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -5,13 +5,16 @@ namespace OpenFeature; /// /// Describes a backed by an . /// -/// The instance. -public sealed record OpenFeatureBuilder(IServiceCollection Services) +/// The services being configured. +public class OpenFeatureBuilder(IServiceCollection services) { + /// The services being configured. + public IServiceCollection Services { get; } = services; + /// /// Indicates whether the evaluation context has been configured. /// This property is used to determine if specific configurations or services /// should be initialized based on the presence of an evaluation context. /// - internal bool IsContextConfigured { get; set; } + public bool IsContextConfigured { get; internal set; } } From 3436db5f022910d39a929040774fc1a20a714b73 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 15:25:45 +0400 Subject: [PATCH 094/123] feat: Implement domain-scoped providers logic --- Directory.Packages.props | 8 +- .../FeatureProviderBuilder.cs | 15 +++ .../Internal/FeatureLifecycleManager.cs | 16 ++- .../MultiTarget/Guard.cs | 6 + .../OpenFeature.DependencyInjection.csproj | 3 +- .../OpenFeatureBuilderExtensions.cs | 125 +++++++++++++++++- .../OpenFeatureOptions.cs | 49 +++++++ .../OpenFeatureServiceCollectionExtensions.cs | 26 ++-- 8 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs create mode 100644 src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8e4347e2..936c04e0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,19 +1,17 @@ - true - + - @@ -30,9 +28,7 @@ - - - + \ No newline at end of file diff --git a/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs b/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs new file mode 100644 index 00000000..65ca94b4 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs @@ -0,0 +1,15 @@ +namespace OpenFeature; + +/// +/// Represents an abstract base class for building feature providers. +/// This builder provides the blueprint for constructing specific instances. +/// +public abstract class FeatureProviderBuilder +{ + /// + /// Constructs and returns an instance of a configured according to the implementation. + /// This method should be implemented by derived classes to return a fully configured instance. + /// + /// An instance of . + public abstract FeatureProvider Build(); +} diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 9472b4f9..0173341e 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace OpenFeature.Internal; @@ -20,12 +21,19 @@ public FeatureLifecycleManager(Api featureApi, IServiceProvider serviceProvider, public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToken = default) { this.LogStartingInitializationOfFeatureProvider(); - var featureProvider = _serviceProvider.GetService(); - if (featureProvider == null) + + var options = _serviceProvider.GetRequiredService>().Value; + if(options.HasDefaultProvider) + { + var featureProvider = _serviceProvider.GetRequiredService(); + await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); + } + + foreach (var name in options.ProviderNames) { - throw new InvalidOperationException("Feature provider is not registered in the service collection."); + var featureProvider = _serviceProvider.GetRequiredKeyedService(name); + await _featureApi.SetProviderAsync(name, featureProvider).ConfigureAwait(false); } - await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); } /// diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs index 8d2726b9..189fe036 100644 --- a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs +++ b/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs @@ -11,4 +11,10 @@ public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameo if (argument is null) throw new ArgumentNullException(paramName); } + + public static void ThrowIfNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (string.IsNullOrWhiteSpace(argument)) + throw new ArgumentNullException(paramName); + } } diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index fcdfd6ad..d3d3e6b1 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net6.0;net8.0;net462 @@ -9,6 +9,7 @@ + diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 8cc8e07a..b0e3143e 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -1,4 +1,6 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using OpenFeature.Model; namespace OpenFeature; @@ -49,17 +51,126 @@ public static OpenFeatureBuilder AddContext( } /// - /// Adds a feature provider to the service collection. + /// Adds a new feature provider with specified options and configuration builder. /// - /// The type of the feature provider, which must inherit from . + /// The type for configuring the feature provider. + /// The type of the provider builder. /// The instance. - /// A factory method to create the feature provider, using the service provider. + /// The action to configure the provider builder. /// The instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Func providerFactory) - where T : FeatureProvider + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action configureBuilder) + where TOptions : OpenFeatureOptions, new() + where TProviderBuilder : FeatureProviderBuilder, new() { - Guard.ThrowIfNull(builder); - builder.Services.TryAddSingleton(providerFactory); + builder.Services.Configure(options => + { + options.AddDefaultProviderName(); + }); + + builder.Services.AddOptions() + .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") + .Configure(configureBuilder); + + builder.Services.TryAddSingleton(static provider => + { + var providerBuilder = provider.GetRequiredService>().Value; + return providerBuilder.Build(); + }); + + builder.AddClient(); + + return builder; + } + + /// + /// Adds a named feature provider with specified options and configuration builder. + /// + /// The type for configuring the feature provider. + /// The type of the provider builder. + /// The instance. + /// The unique name of the provider. + /// The action to configure the provider builder. + /// The instance. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action configureBuilder) + where TOptions : OpenFeatureOptions, new() + where TProviderBuilder : FeatureProviderBuilder, new() + { + Guard.ThrowIfNullOrWhiteSpace(name, nameof(name)); + + builder.Services.Configure(options => + { + options.AddProviderName(name); + }); + + builder.Services.AddOptions(name) + .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") + .Configure(configureBuilder); + + builder.Services.TryAddKeyedSingleton(name, static (provider, key) => + { + var options = provider.GetRequiredService>(); + var providerBuilder = options.Get(key!.ToString()); + return providerBuilder.Build(); + }); + + builder.AddClient(name); + + return builder; + } + + /// + /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. + /// + /// The instance. + /// Optional: The name for the feature client instance. + /// The instance. + internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, string? name = null) + { + if (name == null) + { + if (builder.IsContextConfigured) + { + builder.Services.TryAddScoped(static provider => + { + var api = provider.GetRequiredService(); + var client = api.GetClient(); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + builder.Services.TryAddScoped(static provider => + { + var api = provider.GetRequiredService(); + return api.GetClient(); + }); + } + } + else + { + if (builder.IsContextConfigured) + { + builder.Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + var client = api.GetClient(key!.ToString()); + var context = provider.GetRequiredService(); + client.SetContext(context); + return client; + }); + } + else + { + builder.Services.TryAddKeyedScoped(name, static (provider, key) => + { + var api = provider.GetRequiredService(); + return api.GetClient(key!.ToString()); + }); + } + } + return builder; } } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs new file mode 100644 index 00000000..a3e72737 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -0,0 +1,49 @@ +namespace OpenFeature; + +/// +/// Options to configure OpenFeature +/// +public class OpenFeatureOptions +{ + private static readonly HashSet _providerNames = []; + + /// + /// Determines if a default provider has been registered. + /// + public bool HasDefaultProvider { get; private set; } + + /// + /// The type of the configured feature provider. + /// + public Type FeatureProviderType { get; protected internal set; } = null!; + + /// + /// Gets a read-only list of registered provider names. + /// + public IReadOnlyCollection ProviderNames => _providerNames; + + /// + /// Registers the default provider name if no specific name is provided. + /// Sets to true. + /// + public void AddDefaultProviderName() => AddProviderName(null); + + /// + /// Registers a new feature provider name. This operation is thread-safe. + /// + /// The name of the feature provider to register. Registers as default if null. + public void AddProviderName(string? name) + { + if (name == null) + { + HasDefaultProvider = true; + } + else + { + lock (_providerNames) + { + _providerNames.Add(name); + } + } + } +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 28fb000f..62d65d2e 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using OpenFeature.Internal; -using OpenFeature.Model; namespace OpenFeature; @@ -28,25 +28,15 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services var builder = new OpenFeatureBuilder(services); configure(builder); - if (builder.IsContextConfigured) + services.TryAddScoped(provider => { - services.TryAddScoped(static provider => + var options = provider.GetRequiredService>().Value; + if (!options.HasDefaultProvider) { - var api = provider.GetRequiredService(); - var client = api.GetClient(); - var context = provider.GetRequiredService(); - client.SetContext(context); - return client; - }); - } - else - { - services.TryAddScoped(static provider => - { - var api = provider.GetRequiredService(); - return api.GetClient(); - }); - } + return provider.GetRequiredKeyedService(options.ProviderNames.First()); + } + throw new InvalidOperationException("Default provider is not configured."); + }); return services; } From 5738f3c3ccbebce0e30ef7a03dfc5f0fd5fff4df Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 15:26:40 +0400 Subject: [PATCH 095/123] fix: Correct lines in package.props --- Directory.Packages.props | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 936c04e0..96d64160 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,9 @@ + true + @@ -12,6 +14,7 @@ + @@ -28,7 +31,9 @@ + - \ No newline at end of file + + From 0ce727cb4ce97dac476f931600da23b8170e3548 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 15:31:45 +0400 Subject: [PATCH 096/123] feat(core): Add DependencyInjection namespace for improved modularity --- .../FeatureProviderBuilder.cs | 2 +- .../IFeatureLifecycleManager.cs | 2 +- .../Internal/FeatureLifecycleManager.cs | 2 +- .../OpenFeature.DependencyInjection.csproj | 2 +- .../OpenFeatureBuilder.cs | 2 +- .../OpenFeatureBuilderExtensions.cs | 2 +- .../OpenFeatureOptions.cs | 2 +- .../OpenFeatureServiceCollectionExtensions.cs | 5 +- .../FeatureLifecycleManagerTests.cs | 2 +- .../OpenFeatureBuilderExtensionsTests.cs | 62 +++++++++---------- ...FeatureServiceCollectionExtensionsTests.cs | 2 +- 11 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs b/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs index 65ca94b4..a6b2ee39 100644 --- a/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs +++ b/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs @@ -1,4 +1,4 @@ -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; /// /// Represents an abstract base class for building feature providers. diff --git a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs index 2085bda4..4891f2e8 100644 --- a/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/IFeatureLifecycleManager.cs @@ -1,4 +1,4 @@ -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; /// /// Defines the contract for managing the lifecycle of a feature api. diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 0173341e..12d407ab 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace OpenFeature.Internal; +namespace OpenFeature.DependencyInjection.Internal; internal sealed partial class FeatureLifecycleManager : IFeatureLifecycleManager { diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index d3d3e6b1..701e2cfd 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -4,7 +4,7 @@ netstandard2.0;net6.0;net8.0;net462 enable enable - OpenFeature + OpenFeature.DependencyInjection diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index 6d55dcd0..6fd40f87 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; /// /// Describes a backed by an . diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index b0e3143e..a73e6ecd 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; using OpenFeature.Model; -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; /// /// Contains extension methods for the class. diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs index a3e72737..da6d0fc3 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -1,4 +1,4 @@ -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; /// /// Options to configure OpenFeature diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 62d65d2e..ee0701a0 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -1,14 +1,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using OpenFeature.Internal; +using OpenFeature.DependencyInjection; +using OpenFeature.DependencyInjection.Internal; namespace OpenFeature; /// /// Contains extension methods for the class. /// -public static class OpenFeatureServiceCollectionExtensions +public static partial class OpenFeatureServiceCollectionExtensions { /// /// This method is used to add OpenFeature to the service collection. diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index c18495d3..0783d164 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -1,7 +1,7 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; -using OpenFeature.Internal; +using OpenFeature.DependencyInjection.Internal; using Xunit; namespace OpenFeature.DependencyInjection.Tests; diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index d4726db5..9b6a78f6 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -58,35 +58,35 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe delegateCalled.Should().BeTrue("The delegate should be invoked."); } - [Fact] - public void AddProvider_ShouldAddProviderToCollection() - { - // Act - var result = _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - - // Assert - _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); - _services.Should().ContainSingle(serviceDescriptor => - serviceDescriptor.ServiceType == typeof(FeatureProvider) && - serviceDescriptor.Lifetime == ServiceLifetime.Singleton, - "A singleton service of type FeatureProvider should be added."); - } - - [Fact] - public void AddProvider_ShouldResolveCorrectProvider() - { - // Arrange - _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - - var serviceProvider = _services.BuildServiceProvider(); - - // Act - var provider = serviceProvider.GetService(); - - // Assert - _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); - } + //[Fact] + //public void AddProvider_ShouldAddProviderToCollection() + //{ + // // Act + // var result = _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); + + // // Assert + // _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + // result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + // _services.Should().ContainSingle(serviceDescriptor => + // serviceDescriptor.ServiceType == typeof(FeatureProvider) && + // serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + // "A singleton service of type FeatureProvider should be added."); + //} + + //[Fact] + //public void AddProvider_ShouldResolveCorrectProvider() + //{ + // // Arrange + // _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); + + // var serviceProvider = _services.BuildServiceProvider(); + + // // Act + // var provider = serviceProvider.GetService(); + + // // Assert + // _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + // provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + // provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + //} } diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs index 4149b8a8..d7fec17d 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -3,7 +3,7 @@ using NSubstitute; using Xunit; -namespace OpenFeature.Tests; +namespace OpenFeature.DependencyInjection.Tests; public class OpenFeatureServiceCollectionExtensionsTests { From e8350d7043ec9579d6e3fce4dab3de719877d23c Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 15:33:28 +0400 Subject: [PATCH 097/123] fix(build): Resolve build error in DependencyInjection namespace --- src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs | 1 + src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs index dd9bcbee..574dabae 100644 --- a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; namespace OpenFeature; diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs index 384d0471..b4f2746a 100644 --- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using OpenFeature.DependencyInjection; namespace OpenFeature; From eaf031951f66b1debee57792249c54a6d9746a5a Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 16:04:12 +0400 Subject: [PATCH 098/123] feat(config): Add PolicyName logic to configure default FeatureClient --- .../OpenFeatureBuilderExtensions.cs | 34 +++++++++++++++++++ .../OpenFeatureServiceCollectionExtensions.cs | 18 ++++++---- .../PolicyNameOptions.cs | 12 +++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 src/OpenFeature.DependencyInjection/PolicyNameOptions.cs diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index a73e6ecd..67eea843 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -173,4 +173,38 @@ internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, st return builder; } + + /// + /// Configures a default client for OpenFeature using the provided . + /// + /// The instance. + /// + /// A function to retrieve an based on the service provider and . + /// + /// The configured instance. + internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func getClient) + { + builder.Services.TryAddScoped(provider => + { + var policy = provider.GetRequiredService>().Value; + return getClient(provider, policy); + }); + + return builder; + } + + /// + /// Configures the policy name options for OpenFeature, allowing customization of feature client selection. + /// + /// The instance. + /// A delegate to configure the . + /// The configured instance. + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configureOptions); + + builder.Services.Configure(configureOptions); + return builder; + } } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index ee0701a0..8603ee98 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using OpenFeature.DependencyInjection; using OpenFeature.DependencyInjection.Internal; @@ -29,14 +28,21 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services var builder = new OpenFeatureBuilder(services); configure(builder); - services.TryAddScoped(provider => + + builder.AddDefaultClient((provider, policy) => { - var options = provider.GetRequiredService>().Value; - if (!options.HasDefaultProvider) + if (policy.DefaultNameSelector == null) { - return provider.GetRequiredKeyedService(options.ProviderNames.First()); + return provider.GetRequiredService(); } - throw new InvalidOperationException("Default provider is not configured."); + + var name = policy.DefaultNameSelector.Invoke(provider); + if (name == null) + { + return provider.GetRequiredService(); + } + + return provider.GetRequiredKeyedService(name); }); return services; diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs new file mode 100644 index 00000000..5aef7ce6 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs @@ -0,0 +1,12 @@ +namespace OpenFeature; + +/// +/// Options to configure the default feature client name selection for OpenFeature. +/// +public class PolicyNameOptions +{ + /// + /// A delegate to select the default feature client name based on the service provider context. + /// + public Func? DefaultNameSelector { get; set; } +} From 7916d142211f8ec231cb1b4a0b2c8dbf2b34b735 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 16:06:33 +0400 Subject: [PATCH 099/123] refactor(naming): Rename getClient to clientFactory for clarity --- .../OpenFeatureBuilderExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 67eea843..de0f0ed7 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -175,19 +175,19 @@ internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, st } /// - /// Configures a default client for OpenFeature using the provided . + /// Configures a default client for OpenFeature using the provided factory function. /// /// The instance. - /// - /// A function to retrieve an based on the service provider and . + /// + /// A factory function that creates an based on the service provider and . /// /// The configured instance. - internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func getClient) + internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory) { builder.Services.TryAddScoped(provider => { var policy = provider.GetRequiredService>().Value; - return getClient(provider, policy); + return clientFactory(provider, policy); }); return builder; From af78c5a3c572dd72d7ac69a64c9843a4f17dec3a Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 23:17:28 +0400 Subject: [PATCH 100/123] feat(policy): Implement PolicyName logic for enhanced configuration handling --- .../OpenFeatureBuilder.cs | 40 +++++++++++++++++++ .../OpenFeatureBuilderExtensions.cs | 26 +++++++++--- .../OpenFeatureOptions.cs | 2 +- .../OpenFeatureServiceCollectionExtensions.cs | 37 ++++++++++++----- .../PolicyNameOptions.cs | 8 ++-- 5 files changed, 93 insertions(+), 20 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index 6fd40f87..fcc24aec 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -17,4 +17,44 @@ public class OpenFeatureBuilder(IServiceCollection services) /// should be initialized based on the presence of an evaluation context. /// public bool IsContextConfigured { get; internal set; } + + /// + /// Indicates whether the policy has been configured. + /// + public bool IsPolicyConfigured { get; internal set; } + + /// + /// Gets a value indicating whether a default provider has been registered. + /// + public bool HasDefaultProvider { get; internal set; } + + /// + /// Gets the count of named feature providers that have been registered. + /// This count does not include the default provider. + /// + public int NamedProviderRegistrationCount { get; internal set; } + + /// + /// Validates the current configuration, ensuring that a policy is set when multiple providers are registered + /// or when a default provider is registered alongside another provider. + /// + /// + /// Thrown if multiple providers are registered without a policy, or if both a default provider + /// and an additional provider are registered without a policy configuration. + /// + public void Validate() + { + if (!IsPolicyConfigured) + { + if (NamedProviderRegistrationCount > 1) + { + throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured."); + } + + if (HasDefaultProvider && NamedProviderRegistrationCount == 1) + { + throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration."); + } + } + } } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index de0f0ed7..8abda2db 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using OpenFeature.DependencyInjection; using OpenFeature.Model; -namespace OpenFeature.DependencyInjection; +namespace OpenFeature; /// /// Contains extension methods for the class. @@ -62,6 +63,8 @@ public static OpenFeatureBuilder AddProvider(this Op where TOptions : OpenFeatureOptions, new() where TProviderBuilder : FeatureProviderBuilder, new() { + builder.HasDefaultProvider = true; + builder.Services.Configure(options => { options.AddDefaultProviderName(); @@ -97,6 +100,8 @@ public static OpenFeatureBuilder AddProvider(this Op { Guard.ThrowIfNullOrWhiteSpace(name, nameof(name)); + builder.NamedProviderRegistrationCount++; + builder.Services.Configure(options => { options.AddProviderName(name); @@ -184,7 +189,7 @@ internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, st /// The configured instance. internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder builder, Func clientFactory) { - builder.Services.TryAddScoped(provider => + builder.Services.AddScoped(provider => { var policy = provider.GetRequiredService>().Value; return clientFactory(provider, policy); @@ -194,12 +199,14 @@ internal static OpenFeatureBuilder AddDefaultClient(this OpenFeatureBuilder buil } /// - /// Configures the policy name options for OpenFeature, allowing customization of feature client selection. + /// Configures policy name options for OpenFeature using the specified options type. /// + /// The type of options used to configure . /// The instance. - /// A delegate to configure the . + /// A delegate to configure . /// The configured instance. - public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + where TOptions : PolicyNameOptions { Guard.ThrowIfNull(builder); Guard.ThrowIfNull(configureOptions); @@ -207,4 +214,13 @@ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, builder.Services.Configure(configureOptions); return builder; } + + /// + /// Configures the default policy name options for OpenFeature. + /// + /// The instance. + /// A delegate to configure . + /// The configured instance. + internal static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + => AddPolicyName(builder, configureOptions); } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs index da6d0fc3..a77a1478 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -5,7 +5,7 @@ namespace OpenFeature.DependencyInjection; /// public class OpenFeatureOptions { - private static readonly HashSet _providerNames = []; + private readonly HashSet _providerNames = []; /// /// Determines if a default provider has been registered. diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index 8603ee98..cb350de3 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using OpenFeature.DependencyInjection; using OpenFeature.DependencyInjection.Internal; @@ -11,37 +12,53 @@ namespace OpenFeature; public static partial class OpenFeatureServiceCollectionExtensions { /// - /// This method is used to add OpenFeature to the service collection. - /// OpenFeature will be registered as a singleton. + /// Adds and configures OpenFeature services to the provided . /// - /// - /// the desired configuration - /// the current instance + /// The instance. + /// A configuration action for customizing OpenFeature setup via + /// The modified instance public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure) { Guard.ThrowIfNull(services); Guard.ThrowIfNull(configure); + // Register core OpenFeature services as singletons. services.TryAddSingleton(Api.Instance); services.TryAddSingleton(); var builder = new OpenFeatureBuilder(services); configure(builder); + // If a default provider is specified without additional providers, + // return early as no extra configuration is needed. + if (builder.HasDefaultProvider && builder.NamedProviderRegistrationCount == 0) + { + return services; + } - builder.AddDefaultClient((provider, policy) => + // Validate builder configuration to ensure consistency and required setup. + builder.Validate(); + + if (!builder.IsPolicyConfigured) { - if (policy.DefaultNameSelector == null) + // Add a default name selector policy to use the first registered provider name as the default. + builder.AddPolicyName(options => { - return provider.GetRequiredService(); - } + options.DefaultNameSelector = provider => + { + var options = provider.GetRequiredService>().Value; + return options.ProviderNames.First(); + }; + }); + } + builder.AddDefaultClient((provider, policy) => + { var name = policy.DefaultNameSelector.Invoke(provider); if (name == null) { return provider.GetRequiredService(); } - return provider.GetRequiredKeyedService(name); }); diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs index 5aef7ce6..e017b89a 100644 --- a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs +++ b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs @@ -1,12 +1,12 @@ -namespace OpenFeature; +namespace OpenFeature; /// -/// Options to configure the default feature client name selection for OpenFeature. +/// Options to configure the default feature client name. /// public class PolicyNameOptions { /// - /// A delegate to select the default feature client name based on the service provider context. + /// A delegate to select the default feature client name. /// - public Func? DefaultNameSelector { get; set; } + public Func DefaultNameSelector { get; set; } = null!; } From 4fc100e3152881cc48c59eba2b4f862ede281fb1 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 23:22:46 +0400 Subject: [PATCH 101/123] refactor: Make AddPolicyName extension method public --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 8abda2db..0b556133 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -221,6 +221,6 @@ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder /// The instance. /// A delegate to configure . /// The configured instance. - internal static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) + public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder builder, Action configureOptions) => AddPolicyName(builder, configureOptions); } From d5365151b963342dd0f65e26967103868023cfa9 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Sun, 27 Oct 2024 23:27:31 +0400 Subject: [PATCH 102/123] fix: Update AddPolicyName extension method to set IsPolicyConfigured to true --- .../OpenFeatureBuilderExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 0b556133..588dde45 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -211,6 +211,8 @@ public static OpenFeatureBuilder AddPolicyName(this OpenFeatureBuilder Guard.ThrowIfNull(builder); Guard.ThrowIfNull(configureOptions); + builder.IsPolicyConfigured = true; + builder.Services.Configure(configureOptions); return builder; } From 916f60ade6427d8da1ba57046b04408566598cf6 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 28 Oct 2024 00:02:40 +0400 Subject: [PATCH 103/123] feat(extensions): Add AddProvider extension method with default OpenFeatureOptions --- .../OpenFeatureBuilderExtensions.cs | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 588dde45..9567238d 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -59,7 +59,7 @@ public static OpenFeatureBuilder AddContext( /// The instance. /// The action to configure the provider builder. /// The instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action configureBuilder) + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureBuilder = null) where TOptions : OpenFeatureOptions, new() where TProviderBuilder : FeatureProviderBuilder, new() { @@ -70,9 +70,17 @@ public static OpenFeatureBuilder AddProvider(this Op options.AddDefaultProviderName(); }); - builder.Services.AddOptions() - .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") - .Configure(configureBuilder); + if (configureBuilder != null) + { + builder.Services.AddOptions() + .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") + .Configure(configureBuilder); + } + else + { + builder.Services.AddOptions() + .Configure(options => { }); + } builder.Services.TryAddSingleton(static provider => { @@ -85,6 +93,17 @@ public static OpenFeatureBuilder AddProvider(this Op return builder; } + /// + /// Adds a new feature provider with the default type and a specified configuration builder. + /// + /// The type of the provider builder. + /// The instance. + /// An action to configure the provider builder instance. + /// The configured instance. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureBuilder = null) + where TProviderBuilder : FeatureProviderBuilder, new() + => AddProvider(builder, configureBuilder); + /// /// Adds a named feature provider with specified options and configuration builder. /// @@ -94,7 +113,7 @@ public static OpenFeatureBuilder AddProvider(this Op /// The unique name of the provider. /// The action to configure the provider builder. /// The instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action configureBuilder) + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureBuilder = null) where TOptions : OpenFeatureOptions, new() where TProviderBuilder : FeatureProviderBuilder, new() { @@ -107,9 +126,17 @@ public static OpenFeatureBuilder AddProvider(this Op options.AddProviderName(name); }); - builder.Services.AddOptions(name) - .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") - .Configure(configureBuilder); + if (configureBuilder != null) + { + builder.Services.AddOptions(name) + .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") + .Configure(configureBuilder); + } + else + { + builder.Services.AddOptions(name) + .Configure(options => { }); + } builder.Services.TryAddKeyedSingleton(name, static (provider, key) => { @@ -123,6 +150,18 @@ public static OpenFeatureBuilder AddProvider(this Op return builder; } + /// + /// Adds a named feature provider with a specified configuration builder, using default . + /// + /// The type of the provider builder. + /// The instance. + /// The unique name of the provider. + /// An optional action to configure the provider builder instance. + /// The configured instance. + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureBuilder = null) + where TProviderBuilder : FeatureProviderBuilder, new() + => AddProvider(builder, name, configureBuilder); + /// /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. /// From 387a88b677c307e4590658ca9d61814fe2d5bd75 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 28 Oct 2024 00:03:07 +0400 Subject: [PATCH 104/123] fix(tests): Resolve issues causing test failures --- .../FeatureLifecycleManagerTests.cs | 15 ++++- .../NoOpFeatureProviderBuilder.cs | 6 ++ .../OpenFeatureBuilderExtensionsTests.cs | 62 +++++++++---------- ...FeatureServiceCollectionExtensionsTests.cs | 1 - 4 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs diff --git a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs index 0783d164..b0176bc4 100644 --- a/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs @@ -1,5 +1,7 @@ using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NSubstitute; using OpenFeature.DependencyInjection.Internal; using Xunit; @@ -18,6 +20,13 @@ public FeatureLifecycleManagerTests() _mockServiceProvider = Substitute.For(); + var options = new OpenFeatureOptions(); + options.AddDefaultProviderName(); + var optionsMock = Substitute.For>(); + optionsMock.Value.Returns(options); + + _mockServiceProvider.GetService>().Returns(optionsMock); + _systemUnderTest = new FeatureLifecycleManager( Api.Instance, _mockServiceProvider, @@ -29,8 +38,7 @@ public async Task EnsureInitializedAsync_ShouldLogAndSetProvider_WhenProviderExi { // Arrange var featureProvider = new NoOpFeatureProvider(); - _mockServiceProvider.GetService(typeof(FeatureProvider)) - .Returns(featureProvider); + _mockServiceProvider.GetService(typeof(FeatureProvider)).Returns(featureProvider); // Act await _systemUnderTest.EnsureInitializedAsync().ConfigureAwait(true); @@ -50,6 +58,7 @@ public async Task EnsureInitializedAsync_ShouldThrowException_WhenProviderDoesNo // Assert var exception = await Assert.ThrowsAsync(act).ConfigureAwait(true); - exception.Message.Should().Be("Feature provider is not registered in the service collection."); + exception.Should().NotBeNull(); + exception.Message.Should().NotBeNullOrWhiteSpace(); } } diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs new file mode 100644 index 00000000..9af9d565 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs @@ -0,0 +1,6 @@ +namespace OpenFeature.DependencyInjection.Tests; + +public class NoOpFeatureProviderBuilder : FeatureProviderBuilder +{ + public override FeatureProvider Build() => new NoOpFeatureProvider(); +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 9b6a78f6..8f96c486 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -58,35 +58,35 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe delegateCalled.Should().BeTrue("The delegate should be invoked."); } - //[Fact] - //public void AddProvider_ShouldAddProviderToCollection() - //{ - // // Act - // var result = _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - - // // Assert - // _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - // result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); - // _services.Should().ContainSingle(serviceDescriptor => - // serviceDescriptor.ServiceType == typeof(FeatureProvider) && - // serviceDescriptor.Lifetime == ServiceLifetime.Singleton, - // "A singleton service of type FeatureProvider should be added."); - //} - - //[Fact] - //public void AddProvider_ShouldResolveCorrectProvider() - //{ - // // Arrange - // _systemUnderTest.AddProvider(_ => new NoOpFeatureProvider()); - - // var serviceProvider = _services.BuildServiceProvider(); - - // // Act - // var provider = serviceProvider.GetService(); - - // // Assert - // _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); - // provider.Should().NotBeNull("The FeatureProvider should be resolvable."); - // provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); - //} + [Fact] + public void AddProvider_ShouldAddProviderToCollection() + { + // Act + var result = _systemUnderTest.AddProvider(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + result.Should().BeSameAs(_systemUnderTest, "The method should return the same builder instance."); + _services.Should().ContainSingle(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(FeatureProvider) && + serviceDescriptor.Lifetime == ServiceLifetime.Singleton, + "A singleton service of type FeatureProvider should be added."); + } + + [Fact] + public void AddProvider_ShouldResolveCorrectProvider() + { + // Arrange + _systemUnderTest.AddProvider(); + + var serviceProvider = _services.BuildServiceProvider(); + + // Act + var provider = serviceProvider.GetService(); + + // Assert + _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); + provider.Should().NotBeNull("The FeatureProvider should be resolvable."); + provider.Should().BeOfType("The resolved provider should be of type DefaultFeatureProvider."); + } } diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs index d7fec17d..40e761d2 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureServiceCollectionExtensionsTests.cs @@ -22,7 +22,6 @@ public void AddOpenFeature_ShouldRegisterApiInstanceAndLifecycleManagerAsSinglet // Act _systemUnderTest.AddOpenFeature(_configureAction); - _systemUnderTest.Should().HaveCount(3); _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(Api) && s.Lifetime == ServiceLifetime.Singleton); _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureLifecycleManager) && s.Lifetime == ServiceLifetime.Singleton); _systemUnderTest.Should().ContainSingle(s => s.ServiceType == typeof(IFeatureClient) && s.Lifetime == ServiceLifetime.Scoped); From 4ac7f209795b033e58ec3b1dec7260e1e9411b69 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 28 Oct 2024 20:14:34 +0400 Subject: [PATCH 105/123] refactor(naming): Rename FeatureProviderBuilder to IFeatureProviderFactory for clarity --- .../FeatureProviderBuilder.cs | 15 ----- .../IFeatureProviderFactory.cs | 20 ++++++ .../OpenFeatureBuilderExtensions.cs | 64 +++++++++---------- .../NoOpFeatureProviderBuilder.cs | 6 -- .../NoOpFeatureProviderFactory.cs | 6 ++ .../OpenFeatureBuilderExtensionsTests.cs | 4 +- 6 files changed, 60 insertions(+), 55 deletions(-) delete mode 100644 src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs create mode 100644 src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs delete mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs create mode 100644 test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs diff --git a/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs b/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs deleted file mode 100644 index a6b2ee39..00000000 --- a/src/OpenFeature.DependencyInjection/FeatureProviderBuilder.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace OpenFeature.DependencyInjection; - -/// -/// Represents an abstract base class for building feature providers. -/// This builder provides the blueprint for constructing specific instances. -/// -public abstract class FeatureProviderBuilder -{ - /// - /// Constructs and returns an instance of a configured according to the implementation. - /// This method should be implemented by derived classes to return a fully configured instance. - /// - /// An instance of . - public abstract FeatureProvider Build(); -} diff --git a/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs new file mode 100644 index 00000000..cd4e643a --- /dev/null +++ b/src/OpenFeature.DependencyInjection/IFeatureProviderFactory.cs @@ -0,0 +1,20 @@ +namespace OpenFeature.DependencyInjection; + +/// +/// Provides a contract for creating instances of . +/// This factory interface enables custom configuration and initialization of feature providers +/// to support domain-specific or application-specific feature flag management. +/// +public interface IFeatureProviderFactory +{ + /// + /// Creates an instance of a configured according to + /// the specific settings implemented by the concrete factory. + /// + /// + /// A new instance of . + /// The configuration and behavior of this provider instance are determined by + /// the implementation of this method. + /// + FeatureProvider Create(); +} diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 9567238d..2a0fa88c 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -55,13 +55,13 @@ public static OpenFeatureBuilder AddContext( /// Adds a new feature provider with specified options and configuration builder. /// /// The type for configuring the feature provider. - /// The type of the provider builder. + /// The type of the provider factory implementing . /// The instance. - /// The action to configure the provider builder. + /// An optional action to configure the provider factory of type . /// The instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureBuilder = null) + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) where TOptions : OpenFeatureOptions, new() - where TProviderBuilder : FeatureProviderBuilder, new() + where TProviderFactory : class, IFeatureProviderFactory, new() { builder.HasDefaultProvider = true; @@ -70,22 +70,22 @@ public static OpenFeatureBuilder AddProvider(this Op options.AddDefaultProviderName(); }); - if (configureBuilder != null) + if (configureFactory != null) { - builder.Services.AddOptions() - .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") - .Configure(configureBuilder); + builder.Services.AddOptions() + .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") + .Configure(configureFactory); } else { - builder.Services.AddOptions() + builder.Services.AddOptions() .Configure(options => { }); } builder.Services.TryAddSingleton(static provider => { - var providerBuilder = provider.GetRequiredService>().Value; - return providerBuilder.Build(); + var providerFactory = provider.GetRequiredService>().Value; + return providerFactory.Create(); }); builder.AddClient(); @@ -96,26 +96,26 @@ public static OpenFeatureBuilder AddProvider(this Op /// /// Adds a new feature provider with the default type and a specified configuration builder. /// - /// The type of the provider builder. + /// The type of the provider factory implementing . /// The instance. - /// An action to configure the provider builder instance. + /// An optional action to configure the provider factory of type . /// The configured instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureBuilder = null) - where TProviderBuilder : FeatureProviderBuilder, new() - => AddProvider(builder, configureBuilder); + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, Action? configureFactory = null) + where TProviderFactory : class, IFeatureProviderFactory, new() + => AddProvider(builder, configureFactory); /// /// Adds a named feature provider with specified options and configuration builder. /// /// The type for configuring the feature provider. - /// The type of the provider builder. + /// The type of the provider factory implementing . /// The instance. /// The unique name of the provider. - /// The action to configure the provider builder. + /// An optional action to configure the provider factory of type . /// The instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureBuilder = null) + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureFactory = null) where TOptions : OpenFeatureOptions, new() - where TProviderBuilder : FeatureProviderBuilder, new() + where TProviderFactory : class, IFeatureProviderFactory, new() { Guard.ThrowIfNullOrWhiteSpace(name, nameof(name)); @@ -126,23 +126,23 @@ public static OpenFeatureBuilder AddProvider(this Op options.AddProviderName(name); }); - if (configureBuilder != null) + if (configureFactory != null) { - builder.Services.AddOptions(name) - .Validate(options => options != null, $"{typeof(TProviderBuilder).Name} configuration is invalid.") - .Configure(configureBuilder); + builder.Services.AddOptions(name) + .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") + .Configure(configureFactory); } else { - builder.Services.AddOptions(name) + builder.Services.AddOptions(name) .Configure(options => { }); } builder.Services.TryAddKeyedSingleton(name, static (provider, key) => { - var options = provider.GetRequiredService>(); + var options = provider.GetRequiredService>(); var providerBuilder = options.Get(key!.ToString()); - return providerBuilder.Build(); + return providerBuilder.Create(); }); builder.AddClient(name); @@ -153,14 +153,14 @@ public static OpenFeatureBuilder AddProvider(this Op /// /// Adds a named feature provider with a specified configuration builder, using default . /// - /// The type of the provider builder. + /// The type of the provider factory implementing . /// The instance. /// The unique name of the provider. - /// An optional action to configure the provider builder instance. + /// An optional action to configure the provider factory of type . /// The configured instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureBuilder = null) - where TProviderBuilder : FeatureProviderBuilder, new() - => AddProvider(builder, name, configureBuilder); + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureFactory = null) + where TProviderFactory : class, IFeatureProviderFactory, new() + => AddProvider(builder, name, configureFactory); /// /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs deleted file mode 100644 index 9af9d565..00000000 --- a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderBuilder.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenFeature.DependencyInjection.Tests; - -public class NoOpFeatureProviderBuilder : FeatureProviderBuilder -{ - public override FeatureProvider Build() => new NoOpFeatureProvider(); -} diff --git a/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs new file mode 100644 index 00000000..1ca31cc1 --- /dev/null +++ b/test/OpenFeature.DependencyInjection.Tests/NoOpFeatureProviderFactory.cs @@ -0,0 +1,6 @@ +namespace OpenFeature.DependencyInjection.Tests; + +public class NoOpFeatureProviderFactory : IFeatureProviderFactory +{ + public FeatureProvider Create() => new NoOpFeatureProvider(); +} diff --git a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs index 8f96c486..0921ac46 100644 --- a/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs +++ b/test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs @@ -62,7 +62,7 @@ public void AddContext_Delegate_ShouldCorrectlyHandles(bool useServiceProviderDe public void AddProvider_ShouldAddProviderToCollection() { // Act - var result = _systemUnderTest.AddProvider(); + var result = _systemUnderTest.AddProvider(); // Assert _systemUnderTest.IsContextConfigured.Should().BeFalse("The context should not be configured."); @@ -77,7 +77,7 @@ public void AddProvider_ShouldAddProviderToCollection() public void AddProvider_ShouldResolveCorrectProvider() { // Arrange - _systemUnderTest.AddProvider(); + _systemUnderTest.AddProvider(); var serviceProvider = _services.BuildServiceProvider(); From 17a66ca1d8c8743193fb3c485bb1f034f8daec53 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Mon, 28 Oct 2024 20:59:52 +0400 Subject: [PATCH 106/123] fix(typo): Correct naming typo for improved readability --- .../OpenFeatureBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 2a0fa88c..8815ec72 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -141,8 +141,8 @@ public static OpenFeatureBuilder AddProvider(this Op builder.Services.TryAddKeyedSingleton(name, static (provider, key) => { var options = provider.GetRequiredService>(); - var providerBuilder = options.Get(key!.ToString()); - return providerBuilder.Create(); + var providerFactory = options.Get(key!.ToString()); + return providerFactory.Create(); }); builder.AddClient(name); From c424b2aa890f159162bd6662b220f9e5aa7ce088 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 17:43:27 +0400 Subject: [PATCH 107/123] feat(validation): Add check for whitespace or empty values --- src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs index a77a1478..1be312ed 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs @@ -34,7 +34,7 @@ public class OpenFeatureOptions /// The name of the feature provider to register. Registers as default if null. public void AddProviderName(string? name) { - if (name == null) + if (string.IsNullOrWhiteSpace(name)) { HasDefaultProvider = true; } @@ -42,7 +42,7 @@ public void AddProviderName(string? name) { lock (_providerNames) { - _providerNames.Add(name); + _providerNames.Add(name!); } } } From 1de9096db81e01549d7777e9709a8df15b1edcbb Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 17:45:36 +0400 Subject: [PATCH 108/123] feat(validation): Add check for whitespace or empty values --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 8815ec72..6e9f31c6 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -170,7 +170,7 @@ public static OpenFeatureBuilder AddProvider(this OpenFeatureB /// The instance. internal static OpenFeatureBuilder AddClient(this OpenFeatureBuilder builder, string? name = null) { - if (name == null) + if (string.IsNullOrWhiteSpace(name)) { if (builder.IsContextConfigured) { From ca441157d7ce26c6b08b711025c3ae9c6970b58c Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 20:33:45 +0400 Subject: [PATCH 109/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index fcc24aec..25eee4f1 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -32,7 +32,7 @@ public class OpenFeatureBuilder(IServiceCollection services) /// Gets the count of named feature providers that have been registered. /// This count does not include the default provider. /// - public int NamedProviderRegistrationCount { get; internal set; } + public int DomainBoundProviderRegistrationCount { get; internal set; } /// /// Validates the current configuration, ensuring that a policy is set when multiple providers are registered From 3d33caa563ee5cc5d136db27b6223446d03b9b19 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 20:34:01 +0400 Subject: [PATCH 110/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index 25eee4f1..dee09cfd 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -29,7 +29,7 @@ public class OpenFeatureBuilder(IServiceCollection services) public bool HasDefaultProvider { get; internal set; } /// - /// Gets the count of named feature providers that have been registered. + /// Gets the count of domain-bound providers that have been registered. /// This count does not include the default provider. /// public int DomainBoundProviderRegistrationCount { get; internal set; } From dfd66d1411e09415feffc2f2543c92ac718d9950 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 20:34:11 +0400 Subject: [PATCH 111/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 6e9f31c6..92b1642a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -105,7 +105,7 @@ public static OpenFeatureBuilder AddProvider(this OpenFeatureB => AddProvider(builder, configureFactory); /// - /// Adds a named feature provider with specified options and configuration builder. + /// Adds a feature provider with specified options and configuration builder for the specified domain. /// /// The type for configuring the feature provider. /// The type of the provider factory implementing . From d169b0bb57fcc7c8b618365a279682be2e4ce628 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 20:34:19 +0400 Subject: [PATCH 112/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 92b1642a..8038e75f 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -110,7 +110,7 @@ public static OpenFeatureBuilder AddProvider(this OpenFeatureB /// The type for configuring the feature provider. /// The type of the provider factory implementing . /// The instance. - /// The unique name of the provider. + /// The unique name of the provider. /// An optional action to configure the provider factory of type . /// The instance. public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureFactory = null) From ec6a81becae2a2a0dbfce2fc821f2b8456db3dc1 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 20:34:28 +0400 Subject: [PATCH 113/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 8038e75f..e946b89b 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -151,7 +151,7 @@ public static OpenFeatureBuilder AddProvider(this Op } /// - /// Adds a named feature provider with a specified configuration builder, using default . + /// Adds a feature provider with a specified configuration builder for the specified domain, using default . /// /// The type of the provider factory implementing . /// The instance. From a853b52b7430605a184961b1582064e784771ea1 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Tue, 29 Oct 2024 20:34:36 +0400 Subject: [PATCH 114/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index e946b89b..dcdf010a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -155,7 +155,7 @@ public static OpenFeatureBuilder AddProvider(this Op /// /// The type of the provider factory implementing . /// The instance. - /// The unique name of the provider. + /// The unique domain of the provider. /// An optional action to configure the provider factory of type . /// The configured instance. public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureFactory = null) From 6849899b166753f76292eb47b3101340491117c6 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 30 Oct 2024 07:58:18 +0400 Subject: [PATCH 115/123] Update src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs Co-authored-by: Todd Baert Signed-off-by: Artyom Tonoyan --- .../OpenFeatureBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index dcdf010a..d21b427b 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -113,7 +113,7 @@ public static OpenFeatureBuilder AddProvider(this OpenFeatureB /// The unique name of the provider. /// An optional action to configure the provider factory of type . /// The instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureFactory = null) + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) where TOptions : OpenFeatureOptions, new() where TProviderFactory : class, IFeatureProviderFactory, new() { From 73bb930c472676e1f48c2ff443cab1762360daca Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 30 Oct 2024 08:09:20 +0400 Subject: [PATCH 116/123] fix: Rename argument from name to domain --- .../OpenFeatureBuilderExtensions.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index d21b427b..2f992b56 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -117,35 +117,35 @@ public static OpenFeatureBuilder AddProvider(this Op where TOptions : OpenFeatureOptions, new() where TProviderFactory : class, IFeatureProviderFactory, new() { - Guard.ThrowIfNullOrWhiteSpace(name, nameof(name)); + Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain)); builder.NamedProviderRegistrationCount++; builder.Services.Configure(options => { - options.AddProviderName(name); + options.AddProviderName(domain); }); if (configureFactory != null) { - builder.Services.AddOptions(name) + builder.Services.AddOptions(domain) .Validate(options => options != null, $"{typeof(TProviderFactory).Name} configuration is invalid.") .Configure(configureFactory); } else { - builder.Services.AddOptions(name) + builder.Services.AddOptions(domain) .Configure(options => { }); } - builder.Services.TryAddKeyedSingleton(name, static (provider, key) => + builder.Services.TryAddKeyedSingleton(domain, static (provider, key) => { var options = provider.GetRequiredService>(); var providerFactory = options.Get(key!.ToString()); return providerFactory.Create(); }); - builder.AddClient(name); + builder.AddClient(domain); return builder; } @@ -158,9 +158,9 @@ public static OpenFeatureBuilder AddProvider(this Op /// The unique domain of the provider. /// An optional action to configure the provider factory of type . /// The configured instance. - public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string name, Action? configureFactory = null) + public static OpenFeatureBuilder AddProvider(this OpenFeatureBuilder builder, string domain, Action? configureFactory = null) where TProviderFactory : class, IFeatureProviderFactory, new() - => AddProvider(builder, name, configureFactory); + => AddProvider(builder, domain, configureFactory); /// /// Adds a feature client to the service collection, configuring it to work with a specific context if provided. From 749b18d700651a4646295a38439063aaa1b77fab Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 30 Oct 2024 08:12:45 +0400 Subject: [PATCH 117/123] fix: Rename property to DomainBoundProviderRegistrationCount --- src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs | 4 ++-- .../OpenFeatureBuilderExtensions.cs | 2 +- .../OpenFeatureServiceCollectionExtensions.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs index dee09cfd..ae1e8c8f 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilder.cs @@ -46,12 +46,12 @@ public void Validate() { if (!IsPolicyConfigured) { - if (NamedProviderRegistrationCount > 1) + if (DomainBoundProviderRegistrationCount > 1) { throw new InvalidOperationException("Multiple providers have been registered, but no policy has been configured."); } - if (HasDefaultProvider && NamedProviderRegistrationCount == 1) + if (HasDefaultProvider && DomainBoundProviderRegistrationCount == 1) { throw new InvalidOperationException("A default provider and an additional provider have been registered without a policy configuration."); } diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs index 2f992b56..b54b611a 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs @@ -119,7 +119,7 @@ public static OpenFeatureBuilder AddProvider(this Op { Guard.ThrowIfNullOrWhiteSpace(domain, nameof(domain)); - builder.NamedProviderRegistrationCount++; + builder.DomainBoundProviderRegistrationCount++; builder.Services.Configure(options => { diff --git a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs index cb350de3..7455fe2f 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.DependencyInjection/OpenFeatureServiceCollectionExtensions.cs @@ -31,7 +31,7 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection services // If a default provider is specified without additional providers, // return early as no extra configuration is needed. - if (builder.HasDefaultProvider && builder.NamedProviderRegistrationCount == 0) + if (builder.HasDefaultProvider && builder.DomainBoundProviderRegistrationCount == 0) { return services; } From f6e072d769b7d1f420e194f132c7add21890ef6d Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Wed, 30 Oct 2024 18:54:03 +0400 Subject: [PATCH 118/123] refactor(namespaces): Move Guard class under DependencyInjection namespace --- .../{MultiTarget => }/Guard.cs | 2 +- .../OpenFeature.DependencyInjection.csproj | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) rename src/OpenFeature.DependencyInjection/{MultiTarget => }/Guard.cs (93%) diff --git a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs b/src/OpenFeature.DependencyInjection/Guard.cs similarity index 93% rename from src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs rename to src/OpenFeature.DependencyInjection/Guard.cs index 189fe036..337a8290 100644 --- a/src/OpenFeature.DependencyInjection/MultiTarget/Guard.cs +++ b/src/OpenFeature.DependencyInjection/Guard.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; [DebuggerStepThrough] internal static class Guard diff --git a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj index 701e2cfd..895c45f3 100644 --- a/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj +++ b/src/OpenFeature.DependencyInjection/OpenFeature.DependencyInjection.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net6.0;net8.0;net462 @@ -25,8 +25,4 @@ - - - - From b6a8e2d805262578448fe6d69a6ecaf34f57b78e Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Thu, 31 Oct 2024 12:48:27 +0400 Subject: [PATCH 119/123] feat: Create InMemoryProviderFactory and FeatureBuilderExtensions --- .../Memory/FeatureBuilderExtensions.cs | 52 +++++++++++++++++++ .../Memory/InMemoryProviderFactory.cs | 31 +++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs new file mode 100644 index 00000000..a214f174 --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/FeatureBuilderExtensions.cs @@ -0,0 +1,52 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// Extension methods for configuring feature providers with . +/// +public static partial class FeatureBuilderExtensions +{ + /// + /// Adds an in-memory feature provider to the with optional flag configuration. + /// + /// The instance to configure. + /// + /// An optional delegate to configure feature flags in the in-memory provider. + /// If provided, it allows setting up the initial flags. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, Action>? configure = null) + => builder.AddProvider(factory => ConfigureFlags(factory, configure)); + + /// + /// Adds an in-memory feature provider with a specific domain to the + /// with optional flag configuration. + /// + /// The instance to configure. + /// The unique domain of the provider + /// + /// An optional delegate to configure feature flags in the in-memory provider. + /// If provided, it allows setting up the initial flags. + /// + /// The instance for chaining. + public static OpenFeatureBuilder AddInMemoryProvider(this OpenFeatureBuilder builder, string domain, Action>? configure = null) + => builder.AddProvider(domain, factory => ConfigureFlags(factory, configure)); + + /// + /// Configures the feature flags for an instance. + /// + /// The to configure. + /// + /// An optional delegate that sets up the initial flags in the provider's flag dictionary. + /// + private static void ConfigureFlags(InMemoryProviderFactory factory, Action>? configure) + { + if (configure == null) + return; + + var flag = new Dictionary(); + configure.Invoke(flag); + factory.Flags = flag; + } +} diff --git a/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs new file mode 100644 index 00000000..4841a4df --- /dev/null +++ b/src/OpenFeature.DependencyInjection/Providers/Memory/InMemoryProviderFactory.cs @@ -0,0 +1,31 @@ +using OpenFeature.Providers.Memory; + +namespace OpenFeature.DependencyInjection.Providers.Memory; + +/// +/// A factory for creating instances of , +/// an in-memory implementation of . +/// This factory allows for the customization of feature flags to facilitate +/// testing and lightweight feature flag management without external dependencies. +/// +public class InMemoryProviderFactory : IFeatureProviderFactory +{ + /// + /// Gets or sets the collection of feature flags used to configure the + /// instances. This dictionary maps + /// flag names to instances, enabling pre-configuration + /// of features for testing or in-memory evaluation. + /// + internal IDictionary? Flags { get; set; } + + /// + /// Creates a new instance of with the specified + /// flags set in . This instance is configured for in-memory + /// feature flag management, suitable for testing or lightweight feature toggling scenarios. + /// + /// + /// A configured that can be used to manage + /// feature flags in an in-memory context. + /// + public FeatureProvider Create() => new InMemoryProvider(Flags); +} From f1b8a9bc359d4b83a942551593c9f5614963575d Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 1 Nov 2024 09:07:28 +0400 Subject: [PATCH 120/123] fix(formatting): Correct code formatting issues --- .../Internal/FeatureLifecycleManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs index 12d407ab..d14d421b 100644 --- a/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs +++ b/src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs @@ -23,7 +23,7 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke this.LogStartingInitializationOfFeatureProvider(); var options = _serviceProvider.GetRequiredService>().Value; - if(options.HasDefaultProvider) + if (options.HasDefaultProvider) { var featureProvider = _serviceProvider.GetRequiredService(); await _featureApi.SetProviderAsync(featureProvider).ConfigureAwait(false); From 90481c07b2df4e881028835718e8cdf5f4976087 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 1 Nov 2024 15:57:33 +0400 Subject: [PATCH 121/123] Update src/OpenFeature.DependencyInjection/PolicyNameOptions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- src/OpenFeature.DependencyInjection/PolicyNameOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs index e017b89a..f77b019b 100644 --- a/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs +++ b/src/OpenFeature.DependencyInjection/PolicyNameOptions.cs @@ -1,4 +1,4 @@ -namespace OpenFeature; +namespace OpenFeature.DependencyInjection; /// /// Options to configure the default feature client name. From 8252e47b774c0016c392699225cb999adab1d504 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 1 Nov 2024 15:58:02 +0400 Subject: [PATCH 122/123] Update src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Artyom Tonoyan --- src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs index 574dabae..5209a525 100644 --- a/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs +++ b/src/OpenFeature.Hosting/HostedFeatureLifecycleService.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; using OpenFeature.DependencyInjection; -namespace OpenFeature; +namespace OpenFeature.Hosting; /// /// A hosted service that manages the lifecycle of features within the application. From 9704d2ea175fc114275be74ec6352cfb99f6d7c2 Mon Sep 17 00:00:00 2001 From: Artyom Tonoyan Date: Fri, 1 Nov 2024 16:05:25 +0400 Subject: [PATCH 123/123] fix: Correct namespace issues --- src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs index b4f2746a..16f437b3 100644 --- a/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Hosting/OpenFeatureBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using OpenFeature.DependencyInjection; +using OpenFeature.Hosting; namespace OpenFeature;