From ba0a4e4ffda6f089f6c165d8489d4255ae527091 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:32:21 -0400 Subject: [PATCH 01/43] chore(main): release OpenFeature.Contrib.Providers.Flipt 0.0.5 (#302) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .release-please-manifest.json | 2 +- src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md | 7 +++++++ .../OpenFeature.Contrib.Providers.Flipt.csproj | 2 +- src/OpenFeature.Contrib.Providers.Flipt/version.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6cec9cd1..910b4295 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,5 @@ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", - "src/OpenFeature.Contrib.Providers.Flipt": "0.0.4" + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md index bd4c25af..8756f502 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.4...OpenFeature.Contrib.Providers.Flipt-v0.0.5) (2024-10-18) + + +### 🐛 Bug Fixes + +* update readme ([1aaa387](https://github.com/open-feature/dotnet-sdk-contrib/commit/1aaa3877ae3db884d401226b2138f8e3903a56c2)) + ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.3...OpenFeature.Contrib.Providers.Flipt-v0.0.4) (2024-10-18) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj index a37c3f35..67d695d3 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.Flipt - 0.0.4 + 0.0.5 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt index 81340c7e..bbdeab62 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/version.txt +++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt @@ -1 +1 @@ -0.0.4 +0.0.5 From 7ef76a483e07b33e36a9888349bfc769a21580f2 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 4 Jan 2025 02:52:34 +0000 Subject: [PATCH 02/43] Initial project and test project for aws appconfig provider Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- DotnetSdkContrib.sln | 14 +++++++++++++ .../Class1.cs | 9 +++++++++ ...ture.Contrib.Providers.AwsAppConfig.csproj | 20 +++++++++++++++++++ .../Class1.cs | 6 ++++++ ...Contrib.Providers.AwsAppConfig.Test.csproj | 9 +++++++++ 5 files changed, 58 insertions(+) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln index 57004386..1545da07 100644 --- a/DotnetSdkContrib.sln +++ b/DotnetSdkContrib.sln @@ -45,6 +45,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{B446D481-B5A3-4509-8933-C4CF6DA9B147}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.AwsAppConfig", "src\OpenFeature.Contrib.Providers.AwsAppConfig\OpenFeature.Contrib.Providers.AwsAppConfig.csproj", "{B83B3CA7-7CFD-4915-A5D9-7A88372EA331}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.AwsAppConfig.Test", "test\OpenFeature.Contrib.Providers.AwsAppConfig.Test\OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj", "{222FAE13-8472-4B7A-B6D3-BF07953B953A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -127,6 +131,14 @@ Global {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.Build.0 = Debug|Any CPU {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.ActiveCfg = Release|Any CPU {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.Build.0 = Release|Any CPU + {B83B3CA7-7CFD-4915-A5D9-7A88372EA331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B83B3CA7-7CFD-4915-A5D9-7A88372EA331}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B83B3CA7-7CFD-4915-A5D9-7A88372EA331}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B83B3CA7-7CFD-4915-A5D9-7A88372EA331}.Release|Any CPU.Build.0 = Release|Any CPU + {222FAE13-8472-4B7A-B6D3-BF07953B953A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {222FAE13-8472-4B7A-B6D3-BF07953B953A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {222FAE13-8472-4B7A-B6D3-BF07953B953A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {222FAE13-8472-4B7A-B6D3-BF07953B953A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -151,5 +163,7 @@ Global {F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} {B446D481-B5A3-4509-8933-C4CF6DA9B147} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} + {B83B3CA7-7CFD-4915-A5D9-7A88372EA331} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} + {222FAE13-8472-4B7A-B6D3-BF07953B953A} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} EndGlobalSection EndGlobal diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs new file mode 100644 index 00000000..f9b3a2e6 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs @@ -0,0 +1,9 @@ +using System; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + public class Class1 + { + + } +} diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj new file mode 100644 index 00000000..3e771ce1 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj @@ -0,0 +1,20 @@ + + + + + OpenFeature.Contrib.Providers.AwsAppConfig + 0.0.1 + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + AWS AppConfig provider for .NET + wani-guanxi + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs new file mode 100644 index 00000000..18ad82dc --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs @@ -0,0 +1,6 @@ +namespace OpenFeature.Contrib.Providers.AwsAppConfig.Test; + +public class Class1 +{ + +} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj new file mode 100644 index 00000000..bb23fb7d --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + From 49e6e86002361c4e1c55e7dd6ec8fa9928c5666b Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 4 Jan 2025 02:57:49 +0000 Subject: [PATCH 03/43] added project refrence to test project Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj index bb23fb7d..24ed1d88 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj @@ -1,5 +1,9 @@  + + + + net8.0 enable From bd9fc742d54ea55246716e7d86cdafd09e8076ae Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:41:12 +0000 Subject: [PATCH 04/43] Create AwsAppCofnig Provider Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AwsAppConfigProvider.cs | 240 ++++++++++++++++++ .../Class1.cs | 9 - ...ture.Contrib.Providers.AwsAppConfig.csproj | 7 +- 3 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs delete mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs new file mode 100644 index 00000000..deb9f1ca --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs @@ -0,0 +1,240 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Model; +using Amazon.AppConfigData; +using System.Collections.Generic; +using Amazon.AppConfigData.Model; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + /// + /// OpenFeatures provider for AWS AppConfig that enables feature flag management using AWS AppConfig service. + /// This provider allows fetching and evaluating feature flags stored in AWS AppConfig. + /// + public class AwsAppConfigProvider : FeatureProvider + { + // AWS AppConfig client for interacting with the service + private readonly IAmazonAppConfigData _appConfigClient; + + // The name of the application in AWS AppConfig + private readonly string _applicationName; + + // The environment (e.g., prod, dev, staging) in AWS AppConfig + private readonly string _environmentName; + + // The configuration profile identifier that contains the feature flags + private readonly string _configurationProfileId; + + /// + /// Returns metadata about the provider + /// + /// Metadata object containing provider information + public override Metadata GetMetadata() => new Metadata("AWS AppConfig Provider"); + + + /// + /// Resolves a boolean feature flag value + /// + /// The unique identifier of the feature flag + /// The default value to return if the flag cannot be resolved + /// Additional evaluation context (optional) + /// Cancellation token for async operations + /// Resolution details containing the boolean flag value + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + /// Resolves a double feature flag value + /// + /// The unique identifier of the feature flag + /// The default value to return if the flag cannot be resolved + /// Additional evaluation context (optional) + /// Cancellation token for async operations + /// Resolution details containing the double flag value + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + /// Resolves an integer feature flag value + /// + /// The unique identifier of the feature flag + /// The default value to return if the flag cannot be resolved + /// Additional evaluation context (optional) + /// Cancellation token for async operations + /// Resolution details containing the integer flag value + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + /// Resolves a string feature flag value + /// + /// The unique identifier of the feature flag + /// The default value to return if the flag cannot be resolved + /// Additional evaluation context (optional) + /// Cancellation token for async operations + /// Resolution details containing the string flag value + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + /// Resolves a structured feature flag value + /// + /// The unique identifier of the feature flag + /// The default value to return if the flag cannot be resolved + /// Additional evaluation context (optional) + /// Cancellation token for async operations + /// Resolution details containing the structured flag value + public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + { + var response = await GetFeatureFlagsStreamAsync(); + + // Deserialize the configuration data (assuming JSON format) + var responseString = System.Text.Encoding.UTF8.GetString(response.Configuration.ToArray()); + var flagValue = ParseFeatureFlag(flagKey, defaultValue, responseString); + return await Task.FromResult(new ResolutionDetails(flagKey, new Value(flagValue))); + } + + /// + /// Asynchronously retrieves feature flags configuration from AWS AppConfig using a streaming API. + /// + /// + /// A Task containing GetConfigurationResponse which includes: + /// - The configuration content + /// - Next poll configuration token + /// - Poll interval in seconds + /// + /// + /// This method implements AWS AppConfig's best practices for configuration retrieval: + /// - Uses streaming API for efficient data transfer + /// - Supports incremental updates through configuration tokens + /// - Respects AWS AppConfig's polling interval recommendations + /// + /// The configuration token workflow: + /// 1. Initial call: token = null + /// 2. Subsequent calls: Use token from previous response + /// 3. Token changes when configuration is updated + /// + /// Thrown when AWS AppConfig service encounters an error + /// Thrown when the provider is not properly configured + /// + private async Task GetFeatureFlagsStreamAsync(EvaluationContext context = null) + { + // TODO: Yet to figure out how to pass along Evalutaion Context to AWS AppConfig + + // Build "StartConfigurationSession" Request + var startConfigSessionRequest = new StartConfigurationSessionRequest + { + ApplicationIdentifier = _applicationName, + EnvironmentIdentifier = _environmentName, + ConfigurationProfileIdentifier = _configurationProfileId + }; + + // Start a configuration session with AWS AppConfig + var sessionResponse = await _appConfigClient.StartConfigurationSessionAsync(startConfigSessionRequest); + + // Build "GetLatestConfiguration" request + var configurationRequest = new GetLatestConfigurationRequest + { + ConfigurationToken = sessionResponse.InitialConfigurationToken + }; + + // Get the configuration response from AWS AppConfig + var response = await _appConfigClient.GetLatestConfigurationAsync(configurationRequest); + + return response; + } + + /// + /// Parses a feature flag from a JSON configuration string and converts it to a Value object. + /// + /// The unique identifier of the feature flag to retrieve + /// The default value to return if the flag is not found or cannot be parsed + /// The JSON string containing the feature flag configuration + /// A Value object containing the parsed feature flag value, or the default value if not found + /// + /// The method expects the JSON to be structured as a dictionary where: + /// - The top level contains feature flag keys + /// - Each feature flag value can be a primitive type or a complex object + /// + /// Thrown when the input JSON is invalid or cannot be deserialized + /// + /// + private Value ParseFeatureFlag(string flagKey, Value defaultValue, string inputJson) + { + var parsedJson = JsonSerializer.Deserialize>(inputJson); + if (!parsedJson.TryGetValue(flagKey, out var flagValue)) + return defaultValue; + var parsedItems = JsonSerializer.Deserialize>(flagValue.ToString()); + return ParseChildren(parsedItems); + } + + /// + /// Recursively parses and converts a dictionary of values into a structured Value object. + /// + /// The source dictionary containing key-value pairs to parse + /// A Value object containing the parsed structure + /// + /// This method handles the following scenarios: + /// - Primitive types (int, bool, double, etc.) + /// - String values + /// - Nested dictionaries (converted to structured Values) + /// - Collections/Arrays (converted to list of Values) + /// - Null values + /// + /// For primitive types and strings, it creates a direct Value wrapper. + /// For complex objects, it recursively processes their properties. + /// + private Value ParseChildren(IDictionary children) + { + if(children == null) return null; + IDictionary keyValuePairs = new Dictionary(); + + foreach (var child in children) + { + Type valueType = child.Value.GetType(); + if (valueType.IsValueType || valueType == typeof(string)) + { + keyValuePairs.Add(child.Key, ParseType(child.Value.ToString())); + } + var newChild = JsonSerializer.Deserialize>(child.Value.ToString()); + keyValuePairs.Add(child.Key, ParseChildren(newChild)); + } + return new Value(new Structure(keyValuePairs)); + } + + /// + /// Function to parse string value to a specific type. + /// + /// + /// + private Value ParseType(string value) + { + if (string.IsNullOrWhiteSpace(value)) return new Value(); + + if (bool.TryParse(value, out bool boolValue)) + return new Value(boolValue); + + if (double.TryParse(value, out double doubleValue)) + return new Value(doubleValue); + + if (int.TryParse(value, out int intValue)) + return new Value(intValue); + + if (DateTime.TryParse(value, out DateTime dateTimeValue)) + return new Value(dateTimeValue); + + // if no other type matches, return as string + return new Value(value); + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs deleted file mode 100644 index f9b3a2e6..00000000 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/Class1.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace OpenFeature.Contrib.Providers.AwsAppConfig -{ - public class Class1 - { - - } -} diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj index 3e771ce1..1368bed2 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj @@ -1,8 +1,8 @@  - + net8.0;net7.0;net6.0;netcoreapp3.1; + 7.3 OpenFeature.Contrib.Providers.AwsAppConfig 0.0.1 $(VersionNumber) @@ -17,4 +17,7 @@ <_Parameter1>$(MSBuildProjectName).Test + + + From 2d9cadf2dff4644064b37d8ce46011e0e2fccd26 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Mon, 6 Jan 2025 03:01:16 +0000 Subject: [PATCH 05/43] adding Resolve Function for other types Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AwsAppConfigProvider.cs | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs index deb9f1ca..c186c2d2 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs @@ -42,9 +42,13 @@ public class AwsAppConfigProvider : FeatureProvider /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the boolean flag value - public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override async Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var responseString = await GetFeatureFlagsResponseJson(); + + var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + + return new ResolutionDetails(flagKey, flagValue.AsBoolean ?? defaultValue); } /// @@ -55,9 +59,13 @@ public override Task> ResolveBooleanValueAsync(string fl /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the double flag value - public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var responseString = await GetFeatureFlagsResponseJson(); + + var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + + return new ResolutionDetails(flagKey, flagValue.AsDouble ?? defaultValue); } /// @@ -68,9 +76,13 @@ public override Task> ResolveDoubleValueAsync(string f /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the integer flag value - public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override async Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var responseString = await GetFeatureFlagsResponseJson(); + + var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + + return new ResolutionDetails(flagKey, flagValue.AsInteger ?? defaultValue); } /// @@ -81,9 +93,13 @@ public override Task> ResolveIntegerValueAsync(string fla /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the string flag value - public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) + public override async Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var responseString = await GetFeatureFlagsResponseJson(); + + var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + + return new ResolutionDetails(flagKey, flagValue.AsString ?? defaultValue); } /// @@ -95,15 +111,29 @@ public override Task> ResolveStringValueAsync(string f /// Cancellation token for async operations /// Resolution details containing the structured flag value public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) - { - var response = await GetFeatureFlagsStreamAsync(); + { + var responseString = await GetFeatureFlagsResponseJson(); - // Deserialize the configuration data (assuming JSON format) - var responseString = System.Text.Encoding.UTF8.GetString(response.Configuration.ToArray()); var flagValue = ParseFeatureFlag(flagKey, defaultValue, responseString); return await Task.FromResult(new ResolutionDetails(flagKey, new Value(flagValue))); } + /// + /// Retrieves feature flag configurations as a string (json) from AWS AppConfig. + /// + /// A string containing JSON of the feature flag configuration data from AWS AppConfig. + /// + /// This method fetches the feature flag configuration from AWS AppConfig service + /// and returns it in its raw string format. The returned string is expected to be + /// in JSON format that can be parsed into feature flag configurations. + /// + /// Thrown when there is an error retrieving the configuration from AWS AppConfig. + private async Task GetFeatureFlagsResponseJson() + { + var response = await GetFeatureFlagsStreamAsync(); + return System.Text.Encoding.UTF8.GetString(response.Configuration.ToArray()); + } + /// /// Asynchronously retrieves feature flags configuration from AWS AppConfig using a streaming API. /// @@ -167,7 +197,7 @@ private async Task GetFeatureFlagsStreamAsync(Ev /// - Each feature flag value can be a primitive type or a complex object /// /// Thrown when the input JSON is invalid or cannot be deserialized - /// + /// /// private Value ParseFeatureFlag(string flagKey, Value defaultValue, string inputJson) { @@ -175,13 +205,13 @@ private Value ParseFeatureFlag(string flagKey, Value defaultValue, string inputJ if (!parsedJson.TryGetValue(flagKey, out var flagValue)) return defaultValue; var parsedItems = JsonSerializer.Deserialize>(flagValue.ToString()); - return ParseChildren(parsedItems); + return ParseAttributes(parsedItems); } /// /// Recursively parses and converts a dictionary of values into a structured Value object. /// - /// The source dictionary containing key-value pairs to parse + /// The source dictionary containing key-value pairs to parse /// A Value object containing the parsed structure /// /// This method handles the following scenarios: @@ -194,20 +224,20 @@ private Value ParseFeatureFlag(string flagKey, Value defaultValue, string inputJ /// For primitive types and strings, it creates a direct Value wrapper. /// For complex objects, it recursively processes their properties. /// - private Value ParseChildren(IDictionary children) + private Value ParseAttributes(IDictionary attributes) { - if(children == null) return null; + if(attributes == null) return null; IDictionary keyValuePairs = new Dictionary(); - foreach (var child in children) + foreach (var attribute in attributes) { - Type valueType = child.Value.GetType(); + Type valueType = attribute.Value.GetType(); if (valueType.IsValueType || valueType == typeof(string)) { - keyValuePairs.Add(child.Key, ParseType(child.Value.ToString())); + keyValuePairs.Add(attribute.Key, ParseType(attribute.Value.ToString())); } - var newChild = JsonSerializer.Deserialize>(child.Value.ToString()); - keyValuePairs.Add(child.Key, ParseChildren(newChild)); + var newAttribute = JsonSerializer.Deserialize>(attribute.Value.ToString()); + keyValuePairs.Add(attribute.Key, ParseAttributes(newAttribute)); } return new Value(new Structure(keyValuePairs)); } From 5b11cdf16e26ea6e8cbff5948437c3d788036f36 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Mon, 6 Jan 2025 03:28:56 +0000 Subject: [PATCH 06/43] created separate static class for Aws app config parsing Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AwsAppConfigProvider.cs | 97 ++------------- .../AwsFeatureFlagParser.cs | 114 ++++++++++++++++++ 2 files changed, 121 insertions(+), 90 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs index c186c2d2..a762b27a 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs @@ -34,7 +34,7 @@ public class AwsAppConfigProvider : FeatureProvider public override Metadata GetMetadata() => new Metadata("AWS AppConfig Provider"); - /// + /// /// Resolves a boolean feature flag value /// /// The unique identifier of the feature flag @@ -46,7 +46,7 @@ public override async Task> ResolveBooleanValueAsync(str { var responseString = await GetFeatureFlagsResponseJson(); - var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); return new ResolutionDetails(flagKey, flagValue.AsBoolean ?? defaultValue); } @@ -63,7 +63,7 @@ public override async Task> ResolveDoubleValueAsync(st { var responseString = await GetFeatureFlagsResponseJson(); - var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); return new ResolutionDetails(flagKey, flagValue.AsDouble ?? defaultValue); } @@ -80,7 +80,7 @@ public override async Task> ResolveIntegerValueAsync(stri { var responseString = await GetFeatureFlagsResponseJson(); - var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); return new ResolutionDetails(flagKey, flagValue.AsInteger ?? defaultValue); } @@ -97,7 +97,7 @@ public override async Task> ResolveStringValueAsync(st { var responseString = await GetFeatureFlagsResponseJson(); - var flagValue = ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); + var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); return new ResolutionDetails(flagKey, flagValue.AsString ?? defaultValue); } @@ -114,7 +114,7 @@ public override async Task> ResolveStructureValueAsync( { var responseString = await GetFeatureFlagsResponseJson(); - var flagValue = ParseFeatureFlag(flagKey, defaultValue, responseString); + var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, defaultValue, responseString); return await Task.FromResult(new ResolutionDetails(flagKey, new Value(flagValue))); } @@ -182,89 +182,6 @@ private async Task GetFeatureFlagsStreamAsync(Ev var response = await _appConfigClient.GetLatestConfigurationAsync(configurationRequest); return response; - } - - /// - /// Parses a feature flag from a JSON configuration string and converts it to a Value object. - /// - /// The unique identifier of the feature flag to retrieve - /// The default value to return if the flag is not found or cannot be parsed - /// The JSON string containing the feature flag configuration - /// A Value object containing the parsed feature flag value, or the default value if not found - /// - /// The method expects the JSON to be structured as a dictionary where: - /// - The top level contains feature flag keys - /// - Each feature flag value can be a primitive type or a complex object - /// - /// Thrown when the input JSON is invalid or cannot be deserialized - /// - /// - private Value ParseFeatureFlag(string flagKey, Value defaultValue, string inputJson) - { - var parsedJson = JsonSerializer.Deserialize>(inputJson); - if (!parsedJson.TryGetValue(flagKey, out var flagValue)) - return defaultValue; - var parsedItems = JsonSerializer.Deserialize>(flagValue.ToString()); - return ParseAttributes(parsedItems); - } - - /// - /// Recursively parses and converts a dictionary of values into a structured Value object. - /// - /// The source dictionary containing key-value pairs to parse - /// A Value object containing the parsed structure - /// - /// This method handles the following scenarios: - /// - Primitive types (int, bool, double, etc.) - /// - String values - /// - Nested dictionaries (converted to structured Values) - /// - Collections/Arrays (converted to list of Values) - /// - Null values - /// - /// For primitive types and strings, it creates a direct Value wrapper. - /// For complex objects, it recursively processes their properties. - /// - private Value ParseAttributes(IDictionary attributes) - { - if(attributes == null) return null; - IDictionary keyValuePairs = new Dictionary(); - - foreach (var attribute in attributes) - { - Type valueType = attribute.Value.GetType(); - if (valueType.IsValueType || valueType == typeof(string)) - { - keyValuePairs.Add(attribute.Key, ParseType(attribute.Value.ToString())); - } - var newAttribute = JsonSerializer.Deserialize>(attribute.Value.ToString()); - keyValuePairs.Add(attribute.Key, ParseAttributes(newAttribute)); - } - return new Value(new Structure(keyValuePairs)); - } - - /// - /// Function to parse string value to a specific type. - /// - /// - /// - private Value ParseType(string value) - { - if (string.IsNullOrWhiteSpace(value)) return new Value(); - - if (bool.TryParse(value, out bool boolValue)) - return new Value(boolValue); - - if (double.TryParse(value, out double doubleValue)) - return new Value(doubleValue); - - if (int.TryParse(value, out int intValue)) - return new Value(intValue); - - if (DateTime.TryParse(value, out DateTime dateTimeValue)) - return new Value(dateTimeValue); - - // if no other type matches, return as string - return new Value(value); - } + } } } diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs new file mode 100644 index 00000000..fcc6ab3f --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + /// + /// Provides utility methods for parsing AWS AppConfig feature flag configurations into OpenFeature Value objects. + /// This static class handles the conversion of JSON-formatted feature flag configurations from AWS AppConfig + /// into strongly-typed OpenFeature Value objects. + /// + /// + /// The parser supports the following capabilities: + /// - Parsing of JSON-structured feature flag configurations + /// - Conversion of primitive types (boolean, numeric, string, datetime) + /// - Handling of nested objects and complex structures + /// - Support for default values when flags are not found + /// + /// Type conversion precedence: + /// 1. Boolean + /// 2. Double + /// 3. Integer + /// 4. DateTime + /// 5. String (default fallback) + /// + public static class AwsFeatureFlagParser + { + /// + /// Parses a feature flag from a JSON configuration string and converts it to a Value object. + /// + /// The unique identifier of the feature flag to retrieve + /// The default value to return if the flag is not found or cannot be parsed + /// The JSON string containing the feature flag configuration + /// A Value object containing the parsed feature flag value, or the default value if not found + /// + /// The method expects the JSON to be structured as a dictionary where: + /// - The top level contains feature flag keys + /// - Each feature flag value can be a primitive type or a complex object + /// + /// Thrown when the input JSON is invalid or cannot be deserialized + /// + /// + public static Value ParseFeatureFlag(string flagKey, Value defaultValue, string inputJson) + { + var parsedJson = JsonSerializer.Deserialize>(inputJson); + if (!parsedJson.TryGetValue(flagKey, out var flagValue)) + return defaultValue; + var parsedItems = JsonSerializer.Deserialize>(flagValue.ToString()); + return ParseAttributes(parsedItems); + } + + /// + /// Recursively parses and converts a dictionary of values into a structured Value object. + /// + /// The source dictionary containing key-value pairs to parse + /// A Value object containing the parsed structure + /// + /// This method handles the following scenarios: + /// - Primitive types (int, bool, double, etc.) + /// - String values + /// - Nested dictionaries (converted to structured Values) + /// - Collections/Arrays (converted to list of Values) + /// - Null values + /// + /// For primitive types and strings, it creates a direct Value wrapper. + /// For complex objects, it recursively processes their properties. + /// + private static Value ParseAttributes(IDictionary attributes) + { + if(attributes == null) return null; + IDictionary keyValuePairs = new Dictionary(); + + foreach (var attribute in attributes) + { + Type valueType = attribute.Value.GetType(); + if (valueType.IsValueType || valueType == typeof(string)) + { + keyValuePairs.Add(attribute.Key, ParseValueType(attribute.Value.ToString())); + } + var newAttribute = JsonSerializer.Deserialize>(attribute.Value.ToString()); + keyValuePairs.Add(attribute.Key, ParseAttributes(newAttribute)); + } + return new Value(new Structure(keyValuePairs)); + } + + /// + /// Function to parse string value to a specific type. + /// + /// + /// + private static Value ParseValueType(string value) + { + if (string.IsNullOrWhiteSpace(value)) return new Value(); + + if (bool.TryParse(value, out bool boolValue)) + return new Value(boolValue); + + if (double.TryParse(value, out double doubleValue)) + return new Value(doubleValue); + + if (int.TryParse(value, out int intValue)) + return new Value(intValue); + + if (DateTime.TryParse(value, out DateTime dateTimeValue)) + return new Value(dateTimeValue); + + // if no other type matches, return as string + return new Value(value); + } + } +} + + From 1c1001ad957cfa6c1a2795d590a089dd31b4f5d1 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Mon, 6 Jan 2025 03:36:28 +0000 Subject: [PATCH 07/43] removed unused usings Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AwsAppConfigProvider.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs index a762b27a..92d43f6d 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs @@ -1,10 +1,8 @@ using System; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using OpenFeature.Model; using Amazon.AppConfigData; -using System.Collections.Generic; using Amazon.AppConfigData.Model; namespace OpenFeature.Contrib.Providers.AwsAppConfig @@ -167,7 +165,7 @@ private async Task GetFeatureFlagsStreamAsync(Ev ApplicationIdentifier = _applicationName, EnvironmentIdentifier = _environmentName, ConfigurationProfileIdentifier = _configurationProfileId - }; + }; // Start a configuration session with AWS AppConfig var sessionResponse = await _appConfigClient.StartConfigurationSessionAsync(startConfigSessionRequest); From eb960563ff993c64210c3e0438760eb453258105 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:40:55 +0000 Subject: [PATCH 08/43] added constructor to AppConfig provider Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AwsAppConfigProvider.cs | 26 +++++++++++++++++++ .../AwsFeatureFlagParser.cs | 7 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs index 92d43f6d..3373071e 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs @@ -31,6 +31,32 @@ public class AwsAppConfigProvider : FeatureProvider /// Metadata object containing provider information public override Metadata GetMetadata() => new Metadata("AWS AppConfig Provider"); + /// + /// Constructor for AwsAppConfigProvider + /// + /// The AWS AppConfig client + /// The name of the application in AWS AppConfig + /// The environment (e.g., prod, dev, staging) in AWS AppConfig + /// The configuration profile identifier that contains the feature flags + public AwsAppConfigProvider(IAmazonAppConfigData appConfigClient, string applicationName, string environmentName, string configurationProfileId) + { + // Application name, environment name and configuration profile ID is needed for connecting to AWS Appconfig. + // If any of these are missing, an exception will be thrown. + + if (string.IsNullOrEmpty(applicationName)) + throw new ArgumentNullException(nameof(applicationName)); + + if (string.IsNullOrEmpty(environmentName)) + throw new ArgumentNullException(nameof(environmentName)); + + if (string.IsNullOrEmpty(configurationProfileId)) + throw new ArgumentNullException(nameof(configurationProfileId)); + + _appConfigClient = appConfigClient; + _applicationName = applicationName; + _environmentName = environmentName; + _configurationProfileId = configurationProfileId; + } /// /// Resolves a boolean feature flag value diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs index fcc6ab3f..69b303ce 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs @@ -78,8 +78,11 @@ private static Value ParseAttributes(IDictionary attributes) { keyValuePairs.Add(attribute.Key, ParseValueType(attribute.Value.ToString())); } - var newAttribute = JsonSerializer.Deserialize>(attribute.Value.ToString()); - keyValuePairs.Add(attribute.Key, ParseAttributes(newAttribute)); + else + { + var newAttribute = JsonSerializer.Deserialize>(attribute.Value.ToString()); + keyValuePairs.Add(attribute.Key, ParseAttributes(newAttribute)); + } } return new Value(new Structure(keyValuePairs)); } From ae84a9f9c6804e9438ef94eae235d7162fcc5aa4 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Tue, 7 Jan 2025 04:04:58 +0000 Subject: [PATCH 09/43] cleaned and reused code, and also now accepting key in flagKey:attributeValue format, as supported by AWS app config. Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigKey.cs | 54 ++++++++++++ ...ConfigProvider.cs => AppConfigProvider.cs} | 88 +++++++++++++------ ...tureFlagParser.cs => FeatureFlagParser.cs} | 2 +- 3 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs rename src/OpenFeature.Contrib.Providers.AwsAppConfig/{AwsAppConfigProvider.cs => AppConfigProvider.cs} (69%) rename src/OpenFeature.Contrib.Providers.AwsAppConfig/{AwsFeatureFlagParser.cs => FeatureFlagParser.cs} (97%) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs new file mode 100644 index 00000000..b89af2b3 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs @@ -0,0 +1,54 @@ +using System; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + /// + /// Represents a key structure for AWS AppConfig feature flags with optional attributes. + /// Keys can be in the format "flagKey" or "flagKey:attributeKey" + /// + public class AppConfigKey + { + /// + /// The separator used to split the flag key from its attribute key + /// + private const string Separator = ":"; + + /// + /// Gets the main feature flag key + /// + public string FlagKey { get; } + + /// + /// Gets the optional attribute key associated with the feature flag + /// + public string AttributeKey { get; } + + /// + /// Gets whether this key has an attribute component + /// + public bool HasAttribute => !string.IsNullOrEmpty(AttributeKey); + + /// + /// Initializes a new instance of the AppConfigKey class + /// + /// The composite key string in format "flagKey" or "flagKey:attributeKey" + /// Thrown when the key is null, empty, or consists only of whitespace + public AppConfigKey(string key) + { + if(string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Key cannot be null or empty"); + } + + var parts = key.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + FlagKey = parts[0]; + // At this point, AWS AppConfig allows only value types for attributes. + // Hence ignoring anything afterwords. + if (parts.Length > 1) + { + AttributeKey = parts[1]; + } + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs similarity index 69% rename from src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs rename to src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs index 3373071e..7971464e 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsAppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs @@ -11,7 +11,7 @@ namespace OpenFeature.Contrib.Providers.AwsAppConfig /// OpenFeatures provider for AWS AppConfig that enables feature flag management using AWS AppConfig service. /// This provider allows fetching and evaluating feature flags stored in AWS AppConfig. /// - public class AwsAppConfigProvider : FeatureProvider + public class AppConfigProvider : FeatureProvider { // AWS AppConfig client for interacting with the service private readonly IAmazonAppConfigData _appConfigClient; @@ -38,7 +38,7 @@ public class AwsAppConfigProvider : FeatureProvider /// The name of the application in AWS AppConfig /// The environment (e.g., prod, dev, staging) in AWS AppConfig /// The configuration profile identifier that contains the feature flags - public AwsAppConfigProvider(IAmazonAppConfigData appConfigClient, string applicationName, string environmentName, string configurationProfileId) + public AppConfigProvider(IAmazonAppConfigData appConfigClient, string applicationName, string environmentName, string configurationProfileId) { // Application name, environment name and configuration profile ID is needed for connecting to AWS Appconfig. // If any of these are missing, an exception will be thrown. @@ -61,69 +61,57 @@ public AwsAppConfigProvider(IAmazonAppConfigData appConfigClient, string applica /// /// Resolves a boolean feature flag value /// - /// The unique identifier of the feature flag + /// The feature flag key, which can include an attribute specification in the format "flagKey:attributeKey" /// The default value to return if the flag cannot be resolved /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the boolean flag value public override async Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - var responseString = await GetFeatureFlagsResponseJson(); - - var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); - - return new ResolutionDetails(flagKey, flagValue.AsBoolean ?? defaultValue); + var attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue)); + return new ResolutionDetails(flagKey, attributeValue.AsBoolean ?? defaultValue); } /// /// Resolves a double feature flag value /// - /// The unique identifier of the feature flag + /// The feature flag key, which can include an attribute specification in the format "flagKey:attributeKey" /// The default value to return if the flag cannot be resolved /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the double flag value public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - var responseString = await GetFeatureFlagsResponseJson(); - - var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); - - return new ResolutionDetails(flagKey, flagValue.AsDouble ?? defaultValue); + var attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue)); + return new ResolutionDetails(flagKey, attributeValue.AsDouble ?? defaultValue); } /// /// Resolves an integer feature flag value /// - /// The unique identifier of the feature flag + /// The feature flag key, which can include an attribute specification in the format "flagKey:attributeKey" /// The default value to return if the flag cannot be resolved /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the integer flag value public override async Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - var responseString = await GetFeatureFlagsResponseJson(); - - var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); - - return new ResolutionDetails(flagKey, flagValue.AsInteger ?? defaultValue); + var attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue)); + return new ResolutionDetails(flagKey, attributeValue.AsInteger ?? defaultValue); } /// /// Resolves a string feature flag value /// - /// The unique identifier of the feature flag + /// The feature flag key, which can include an attribute specification in the format "flagKey:attributeKey" /// The default value to return if the flag cannot be resolved /// Additional evaluation context (optional) /// Cancellation token for async operations /// Resolution details containing the string flag value public override async Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { - var responseString = await GetFeatureFlagsResponseJson(); - - var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, new Value(defaultValue), responseString); - - return new ResolutionDetails(flagKey, flagValue.AsString ?? defaultValue); + var attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue)); + return new ResolutionDetails(flagKey, attributeValue.AsString ?? defaultValue); } /// @@ -135,13 +123,55 @@ public override async Task> ResolveStringValueAsync(st /// Cancellation token for async operations /// Resolution details containing the structured flag value public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) - { + { + var flagValue = await ResolveFeatureFlagValue(flagKey, defaultValue); + return new ResolutionDetails(flagKey, new Value(flagValue)); + } + + /// + /// Resolves a feature flag value from AWS AppConfig, optionally extracting a specific attribute. + /// + /// The feature flag key, which can include an attribute specification in the format "flagKey:attributeKey" + /// The default value to return if the flag or attribute cannot be resolved + /// + /// A Value object containing the resolved feature flag value. If the key includes an attribute specification, + /// returns the value of that attribute. Otherwise, returns the entire flag value. + /// + /// + /// This method handles two types of feature flag resolution: + /// 1. Simple flag resolution: When flagKey is a simple key (e.g., "myFlag") + /// 2. Attribute resolution: When flagKey includes an attribute specification (e.g., "myFlag:someAttribute") + /// + /// The method first retrieves the complete feature flag configuration and then: + /// - For simple flags: Returns the entire flag value + /// - For attribute-based flags: Returns the specific attribute value if it exists, otherwise returns the default value + /// + /// + /// Simple flag usage: + /// + /// var value = await ResolveFeatureFlagValue("myFlag", new Value(defaultValue)); + /// + /// + /// Attribute-based usage: + /// + /// var value = await ResolveFeatureFlagValue("myFlag:color", new Value("blue")); + /// + /// + private async Task ResolveFeatureFlagValue(string flagKey, Value defaultValue) + { var responseString = await GetFeatureFlagsResponseJson(); + var appConfigKey = new AppConfigKey(flagKey); - var flagValue = AwsFeatureFlagParser.ParseFeatureFlag(flagKey, defaultValue, responseString); - return await Task.FromResult(new ResolutionDetails(flagKey, new Value(flagValue))); + var flagValues = FeatureFlagParser.ParseFeatureFlag(appConfigKey.FlagKey, defaultValue, responseString); + + if(appConfigKey.HasAttribute) + { + return flagValues.AsStructure.TryGetValue(appConfigKey.AttributeKey, out var returnValue) ? returnValue: defaultValue; + } + return flagValues; } + /// /// Retrieves feature flag configurations as a string (json) from AWS AppConfig. /// diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagParser.cs similarity index 97% rename from src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs rename to src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagParser.cs index 69b303ce..2045c7a6 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AwsFeatureFlagParser.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagParser.cs @@ -24,7 +24,7 @@ namespace OpenFeature.Contrib.Providers.AwsAppConfig /// 4. DateTime /// 5. String (default fallback) /// - public static class AwsFeatureFlagParser + public static class FeatureFlagParser { /// /// Parses a feature flag from a JSON configuration string and converts it to a Value object. From ca4479a46f2b32e5d2727aec8187bebc2e587c9e Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 9 Jan 2025 02:48:04 +0000 Subject: [PATCH 10/43] put some more checkts in ResolveFeatureFlagValue function Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigProvider.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs index 7971464e..1749812a 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs @@ -164,11 +164,13 @@ private async Task ResolveFeatureFlagValue(string flagKey, Value defaultV var flagValues = FeatureFlagParser.ParseFeatureFlag(appConfigKey.FlagKey, defaultValue, responseString); - if(appConfigKey.HasAttribute) - { - return flagValues.AsStructure.TryGetValue(appConfigKey.AttributeKey, out var returnValue) ? returnValue: defaultValue; - } - return flagValues; + if (!appConfigKey.HasAttribute) return flagValues; + + var structuredValues = flagValues.AsStructure; + + if(structuredValues == null) return defaultValue; + + return structuredValues.TryGetValue(appConfigKey.AttributeKey, out var returnValue) ? returnValue : defaultValue; } From 81716b839d39d151176d9de605daaaeb536c7f61 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 9 Jan 2025 04:18:54 +0000 Subject: [PATCH 11/43] Adding AppConfigRetriebalAPI class with memory caching for app config values Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigRetrievalApi.cs | 153 ++++++++++++++++++ .../FeatureFlagProfile.cs | 49 ++++++ ...ture.Contrib.Providers.AwsAppConfig.csproj | 1 + 3 files changed, 203 insertions(+) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs new file mode 100644 index 00000000..f4948b43 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs @@ -0,0 +1,153 @@ +using System; +using System.Threading.Tasks; +using Amazon.AppConfigData; +using Microsoft.Extensions.Caching.Memory; +using Amazon.AppConfigData.Model; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + /// + /// Provides functionality to interact with AWS AppConfig Data API with built-in memory caching support. + /// This class handles configuration retrieval and session management for AWS AppConfig feature flags. + /// + /// + /// This class implements caching mechanisms to optimize performance and reduce API calls to AWS AppConfig. + /// Key features: + /// - Caches configuration responses and session data + /// - Configurable cache duration (defaults to 5 minutes) + /// - Thread-safe cache operations using GetOrCreateAsync + /// - Supports manual cache invalidation + /// - Implements IDisposable for proper resource cleanup + /// + public class AppConfigRetrievalApi + { + private const string SESSION_TOKEN_KEY_PREFIX = "session_token"; + private const string CONFIGURATION_VALUE_KEY_PREFIX = "config_value"; + private const double DEFAULT_CACHE_DURATION_MINUTES = 60; + + /// + /// The AWS AppConfig Data client used to interact with the AWS AppConfig service. + /// + private readonly IAmazonAppConfigData _appConfigDataClient; + + /// + /// The memory cache instance used for storing configuration and session data. + /// + private readonly IMemoryCache _memoryCache; + + /// + /// Cache entry options defining how items are cached, including expiration settings. + /// + private readonly MemoryCacheEntryOptions _cacheOptions; + + + /// + /// Initializes a new instance of the AppConfigRetrievalApi class. + /// + /// The AWS AppConfig Data client used to interact with the AWS AppConfig service. + /// Optional duration for which items should be cached. Defaults to 5 minutes if not specified. + /// Thrown when appConfigDataClient is null. + public AppConfigRetrievalApi(IAmazonAppConfigData appConfigDataClient, TimeSpan? cacheDuration = null) + { + _appConfigDataClient = appConfigDataClient ?? throw new ArgumentNullException(nameof(appConfigDataClient)); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); + + // Default cache duration of 60 minutes if not specified + _cacheOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(cacheDuration ?? TimeSpan.FromMinutes(DEFAULT_CACHE_DURATION_MINUTES)); + } + + /// + /// Retrieves configuration from AWS AppConfig using the provided configuration token. + /// Results are cached based on the configured cache duration. + /// + /// The configuration token obtained from a configuration session. + /// A task that represents the asynchronous operation. The task result contains the configuration response. + /// + /// The configuration is cached using the configuration token as part of the cache key. + /// Subsequent calls with the same token will return the cached result until it expires. + /// + public async TaskGetLatestConfigurationAsync(FeatureFlagProfile profile) + { + var configKey = BuildConfigurationKey(profile); + var sessionKey = BuildSessionKey(profile); + + // Build GetLatestConfiguration Request + var configurationRequest = new GetLatestConfigurationRequest + { + ConfigurationToken = await GetSessionToken(profile) + }; + + var response = await _appConfigDataClient.GetLatestConfigurationAsync(configurationRequest); + + // If not NextPollConfigurationToken, something wrong with AWS connection. + if(string.IsNullOrWhiteSpace(response.NextPollConfigurationToken)) throw new Exception("Unable to connect to AWS"); + + // First, update the session token to the newly returned token + _memoryCache.Set(sessionKey, response.NextPollConfigurationToken); + + if(response.Configuration == null && _memoryCache.TryGetValue(configKey, out GetLatestConfigurationResponse configValue)) + { + // AppConfig returns null for Configuration if value hasn't changed from last retrieval, hence use what's in cache. + return configValue; + } + else + { + // Set the new value returned from AWS. + _memoryCache.Set(configKey, response); + return response; + } + + + } + + public void InvalidateConfigurationCache(FeatureFlagProfile profile) + { + _memoryCache.Remove(BuildConfigurationKey(profile)); + } + + public void InvalidateSessionCache(FeatureFlagProfile profile) + { + _memoryCache.Remove(BuildSessionKey(profile)); + } + + // Implement IDisposable to properly clean up the MemoryCache + public void Dispose() + { + if (_memoryCache is IDisposable disposableCache) + { + disposableCache.Dispose(); + } + } + private async Task GetSessionToken(FeatureFlagProfile profile) + { + if(!profile.IsValid) throw new ArgumentException("Invalid Feature Flag configuration profile"); + + return await _memoryCache.GetOrCreateAsync(BuildSessionKey(profile), async entry => + { + entry.SetOptions(_cacheOptions); + + var request = new StartConfigurationSessionRequest + { + ApplicationIdentifier = profile.ApplicationIdentifier, + EnvironmentIdentifier = profile.EnvironmentIdentifier, + ConfigurationProfileIdentifier = profile.ConfigurationProfileIdentifier + }; + + var sessionResponse = await _appConfigDataClient.StartConfigurationSessionAsync(request); + // We only need Initial Configuration Token from starting the session. + return sessionResponse.InitialConfigurationToken; + }); + } + + private string BuildSessionKey(FeatureFlagProfile profile) + { + return $"{SESSION_TOKEN_KEY_PREFIX}_{profile}"; + } + + private string BuildConfigurationKey(FeatureFlagProfile profile) + { + return $"{CONFIGURATION_VALUE_KEY_PREFIX}_{profile}"; + } + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs new file mode 100644 index 00000000..c28c126b --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs @@ -0,0 +1,49 @@ +using System; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + /// + /// Represents a configuration profile for AWS AppConfig feature flags. + /// This class contains the necessary identifiers to uniquely identify and access + /// a feature flag configuration in AWS AppConfig. + /// + public class FeatureFlagProfile + { + /// + /// Gets or sets the AWS AppConfig application identifier. + /// This is the unique identifier for the application in AWS AppConfig. + /// + public string ApplicationIdentifier { get; set; } + + /// + /// Gets or sets the AWS AppConfig environment identifier. + /// This represents the deployment environment (e.g., development, production) in AWS AppConfig. + /// + public string EnvironmentIdentifier { get; set; } + + /// + /// Gets or sets the AWS AppConfig configuration profile identifier. + /// This identifies the specific configuration profile containing the feature flags. + /// + public string ConfigurationProfileIdentifier { get; set; } + + /// + /// Gets a value indicating whether the profile is valid. + /// A profile is considered valid when all identifiers (Application, Environment, and Configuration Profile) + /// are non-null and non-empty. + /// + public bool IsValid => !(string.IsNullOrEmpty(ApplicationIdentifier) || + string.IsNullOrEmpty(EnvironmentIdentifier) || + string.IsNullOrEmpty(ConfigurationProfileIdentifier)); + + /// + /// Returns a string representation of the feature flag profile. + /// The format is "ApplicationIdentifier+EnvironmentIdentifier+ConfigurationProfileIdentifier". + /// + /// A string containing all three identifiers concatenated with '+' characters. + public override string ToString() + { + return $"{ApplicationIdentifier}_{EnvironmentIdentifier}_{ConfigurationProfileIdentifier}"; + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj index 1368bed2..42186d4d 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj @@ -19,5 +19,6 @@ + From dd40afafd551347e697610eac2cdfd0d589e8ab9 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 9 Jan 2025 19:10:35 +0000 Subject: [PATCH 12/43] updating constructor and how configuration profile id is passed along Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigKey.cs | 56 ++++++++++-- .../AppConfigProvider.cs | 88 ++++++++----------- .../AppConfigRetrievalApi.cs | 85 +++++++++++++++--- .../IRetrievalApi.cs | 42 +++++++++ 4 files changed, 201 insertions(+), 70 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/IRetrievalApi.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs index b89af2b3..e222e39c 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs @@ -13,6 +13,11 @@ public class AppConfigKey /// private const string Separator = ":"; + /// + /// Gets the App config's Configuration Profile ID + /// + public string ConfigurationProfileId {get; } + /// /// Gets the main feature flag key /// @@ -29,10 +34,43 @@ public class AppConfigKey public bool HasAttribute => !string.IsNullOrEmpty(AttributeKey); /// - /// Initializes a new instance of the AppConfigKey class + /// Initializes a new instance of the class that represents a structured key + /// for AWS AppConfig feature flags. /// - /// The composite key string in format "flagKey" or "flagKey:attributeKey" - /// Thrown when the key is null, empty, or consists only of whitespace + /// + /// The composite key string that must be in the format "configurationProfileId:flagKey[:attributeKey]" where: + /// + /// configurationProfileId - The AWS AppConfig configuration profile identifier + /// flagKey - The feature flag key name + /// attributeKey - (Optional) The specific attribute key to access within the feature flag + /// + /// + /// + /// Thrown when: + /// + /// The key parameter is null, empty, or consists only of whitespace + /// The key format is invalid (missing required parts) + /// The key doesn't contain at least configurationProfileId and flagKey parts + /// + /// + /// + /// The constructor parses the provided key string and populates the corresponding properties: + /// + /// - First part of the key + /// - Second part of the key + /// - Third part of the key (if provided) + /// + /// + /// + /// Valid key formats: + /// + /// // Basic usage with configuration profile and flag key + /// var key1 = new AppConfigKey("myProfile:myFlag"); + /// + /// // Usage with an attribute key + /// var key2 = new AppConfigKey("myProfile:myFlag:myAttribute"); + /// + /// public AppConfigKey(string key) { if(string.IsNullOrWhiteSpace(key)) @@ -41,13 +79,19 @@ public AppConfigKey(string key) } var parts = key.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if(parts.Length < 2 ) + { + throw new ArgumentException("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format"); + } - FlagKey = parts[0]; + ConfigurationProfileId = parts[0]; + FlagKey = parts[1]; // At this point, AWS AppConfig allows only value types for attributes. // Hence ignoring anything afterwords. - if (parts.Length > 1) + if (parts.Length > 2) { - AttributeKey = parts[1]; + AttributeKey = parts[2]; } } } diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs index 1749812a..b86b8e5e 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs @@ -14,16 +14,13 @@ namespace OpenFeature.Contrib.Providers.AwsAppConfig public class AppConfigProvider : FeatureProvider { // AWS AppConfig client for interacting with the service - private readonly IAmazonAppConfigData _appConfigClient; + private readonly IRetrievalApi _appConfigRetrievalApi; // The name of the application in AWS AppConfig private readonly string _applicationName; // The environment (e.g., prod, dev, staging) in AWS AppConfig - private readonly string _environmentName; - - // The configuration profile identifier that contains the feature flags - private readonly string _configurationProfileId; + private readonly string _environmentName; /// /// Returns metadata about the provider @@ -34,11 +31,10 @@ public class AppConfigProvider : FeatureProvider /// /// Constructor for AwsAppConfigProvider /// - /// The AWS AppConfig client + /// The AWS AppConfig retrieval API /// The name of the application in AWS AppConfig - /// The environment (e.g., prod, dev, staging) in AWS AppConfig - /// The configuration profile identifier that contains the feature flags - public AppConfigProvider(IAmazonAppConfigData appConfigClient, string applicationName, string environmentName, string configurationProfileId) + /// The environment (e.g., prod, dev, staging) in AWS AppConfig + public AppConfigProvider(IRetrievalApi retrievalApi, string applicationName, string environmentName) { // Application name, environment name and configuration profile ID is needed for connecting to AWS Appconfig. // If any of these are missing, an exception will be thrown. @@ -47,15 +43,11 @@ public AppConfigProvider(IAmazonAppConfigData appConfigClient, string applicatio throw new ArgumentNullException(nameof(applicationName)); if (string.IsNullOrEmpty(environmentName)) - throw new ArgumentNullException(nameof(environmentName)); - - if (string.IsNullOrEmpty(configurationProfileId)) - throw new ArgumentNullException(nameof(configurationProfileId)); - - _appConfigClient = appConfigClient; + throw new ArgumentNullException(nameof(environmentName)); + + _appConfigRetrievalApi = retrievalApi; _applicationName = applicationName; - _environmentName = environmentName; - _configurationProfileId = configurationProfileId; + _environmentName = environmentName; } /// @@ -159,9 +151,10 @@ public override async Task> ResolveStructureValueAsync( /// private async Task ResolveFeatureFlagValue(string flagKey, Value defaultValue) { - var responseString = await GetFeatureFlagsResponseJson(); var appConfigKey = new AppConfigKey(flagKey); + var responseString = await GetFeatureFlagsResponseJson(appConfigKey.ConfigurationProfileId); + var flagValues = FeatureFlagParser.ParseFeatureFlag(appConfigKey.FlagKey, defaultValue, responseString); if (!appConfigKey.HasAttribute) return flagValues; @@ -184,9 +177,9 @@ private async Task ResolveFeatureFlagValue(string flagKey, Value defaultV /// in JSON format that can be parsed into feature flag configurations. /// /// Thrown when there is an error retrieving the configuration from AWS AppConfig. - private async Task GetFeatureFlagsResponseJson() + private async Task GetFeatureFlagsResponseJson(string configurationProfileId, EvaluationContext context = null) { - var response = await GetFeatureFlagsStreamAsync(); + var response = await GetFeatureFlagsStreamAsync(configurationProfileId, context); return System.Text.Encoding.UTF8.GetString(response.Configuration.ToArray()); } @@ -200,44 +193,39 @@ private async Task GetFeatureFlagsResponseJson() /// - Poll interval in seconds /// /// - /// This method implements AWS AppConfig's best practices for configuration retrieval: - /// - Uses streaming API for efficient data transfer - /// - Supports incremental updates through configuration tokens - /// - Respects AWS AppConfig's polling interval recommendations - /// - /// The configuration token workflow: - /// 1. Initial call: token = null - /// 2. Subsequent calls: Use token from previous response - /// 3. Token changes when configuration is updated - /// - /// Thrown when AWS AppConfig service encounters an error - /// Thrown when the provider is not properly configured - /// - private async Task GetFeatureFlagsStreamAsync(EvaluationContext context = null) + private async Task GetFeatureFlagsStreamAsync(string configurationProfileId, EvaluationContext context = null) { - // TODO: Yet to figure out how to pass along Evalutaion Context to AWS AppConfig - - // Build "StartConfigurationSession" Request - var startConfigSessionRequest = new StartConfigurationSessionRequest + var profile = new FeatureFlagProfile { ApplicationIdentifier = _applicationName, EnvironmentIdentifier = _environmentName, - ConfigurationProfileIdentifier = _configurationProfileId - }; + ConfigurationProfileIdentifier = configurationProfileId + }; - // Start a configuration session with AWS AppConfig - var sessionResponse = await _appConfigClient.StartConfigurationSessionAsync(startConfigSessionRequest); + return await _appConfigRetrievalApi.GetLatestConfigurationAsync(profile); + // // TODO: Yet to figure out how to pass along Evalutaion Context to AWS AppConfig - // Build "GetLatestConfiguration" request - var configurationRequest = new GetLatestConfigurationRequest - { - ConfigurationToken = sessionResponse.InitialConfigurationToken - }; + // // Build "StartConfigurationSession" Request + // var startConfigSessionRequest = new StartConfigurationSessionRequest + // { + // ApplicationIdentifier = _applicationName, + // EnvironmentIdentifier = _environmentName, + // ConfigurationProfileIdentifier = _configurationProfileId + // }; + + // // Start a configuration session with AWS AppConfig + // var sessionResponse = await _appConfigClient.StartConfigurationSessionAsync(startConfigSessionRequest); + + // // Build "GetLatestConfiguration" request + // var configurationRequest = new GetLatestConfigurationRequest + // { + // ConfigurationToken = sessionResponse.InitialConfigurationToken + // }; - // Get the configuration response from AWS AppConfig - var response = await _appConfigClient.GetLatestConfigurationAsync(configurationRequest); + // // Get the configuration response from AWS AppConfig + // var response = await _appConfigClient.GetLatestConfigurationAsync(configurationRequest); - return response; + // return response; } } } diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs index f4948b43..be047907 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs @@ -19,19 +19,30 @@ namespace OpenFeature.Contrib.Providers.AwsAppConfig /// - Supports manual cache invalidation /// - Implements IDisposable for proper resource cleanup /// - public class AppConfigRetrievalApi + public class AppConfigRetrievalApi: IRetrievalApi { + /// + /// Prefix used for session token cache keys to prevent key collisions. + /// private const string SESSION_TOKEN_KEY_PREFIX = "session_token"; + + /// + /// Prefix used for configuration value cache keys to prevent key collisions. + /// private const string CONFIGURATION_VALUE_KEY_PREFIX = "config_value"; + + /// + /// Default cache duration in minutes for configuration and session data. + /// private const double DEFAULT_CACHE_DURATION_MINUTES = 60; /// - /// The AWS AppConfig Data client used to interact with the AWS AppConfig service. + /// AWS AppConfig Data client used to interact with the AWS AppConfig service. /// private readonly IAmazonAppConfigData _appConfigDataClient; /// - /// The memory cache instance used for storing configuration and session data. + /// Memory cache instance used for storing configuration and session data. /// private readonly IMemoryCache _memoryCache; @@ -58,15 +69,17 @@ public AppConfigRetrievalApi(IAmazonAppConfigData appConfigDataClient, TimeSpan? } /// - /// Retrieves configuration from AWS AppConfig using the provided configuration token. + /// Retrieves configuration from AWS AppConfig using the provided feature flag profile. /// Results are cached based on the configured cache duration. /// - /// The configuration token obtained from a configuration session. + /// The feature flag profile containing application, environment, and configuration identifiers. /// A task that represents the asynchronous operation. The task result contains the configuration response. /// - /// The configuration is cached using the configuration token as part of the cache key. - /// Subsequent calls with the same token will return the cached result until it expires. + /// The configuration is cached using the profile information as part of the cache key. + /// If AWS returns an empty configuration, it indicates no changes from the previous configuration, + /// and the cached value will be returned if available. /// + /// Thrown when unable to connect to AWS or retrieve configuration. public async TaskGetLatestConfigurationAsync(FeatureFlagProfile profile) { var configKey = BuildConfigurationKey(profile); @@ -86,9 +99,10 @@ public async TaskGetLatestConfigurationAsync(Fea // First, update the session token to the newly returned token _memoryCache.Set(sessionKey, response.NextPollConfigurationToken); - if(response.Configuration == null && _memoryCache.TryGetValue(configKey, out GetLatestConfigurationResponse configValue)) + if((response.Configuration == null || response.Configuration.Length == 0) + && _memoryCache.TryGetValue(configKey, out GetLatestConfigurationResponse configValue)) { - // AppConfig returns null for Configuration if value hasn't changed from last retrieval, hence use what's in cache. + // AppConfig returns empty Configuration if value hasn't changed from last retrieval, hence use what's in cache. return configValue; } else @@ -96,22 +110,41 @@ public async TaskGetLatestConfigurationAsync(Fea // Set the new value returned from AWS. _memoryCache.Set(configKey, response); return response; - } - - - } + } + } + /// + /// Invalidates the cached configuration for the specified feature flag profile. + /// + /// The feature flag profile whose configuration cache should be invalidated. + /// + /// This method forces the next GetLatestConfigurationAsync call to fetch fresh data from AWS AppConfig + /// instead of using cached values. + /// public void InvalidateConfigurationCache(FeatureFlagProfile profile) { _memoryCache.Remove(BuildConfigurationKey(profile)); } + /// + /// Invalidates the cached session token for the specified feature flag profile. + /// + /// The feature flag profile whose session token cache should be invalidated. + /// + /// This method forces the next operation to create a new session with AWS AppConfig + /// instead of using the cached session token. + /// public void InvalidateSessionCache(FeatureFlagProfile profile) { _memoryCache.Remove(BuildSessionKey(profile)); } - // Implement IDisposable to properly clean up the MemoryCache + /// + /// Releases all resources used by the AppConfigRetrievalApi instance. + /// + /// + /// This method ensures proper cleanup of the memory cache when the instance is disposed. + /// public void Dispose() { if (_memoryCache is IDisposable disposableCache) @@ -119,6 +152,16 @@ public void Dispose() disposableCache.Dispose(); } } + + /// + /// Retrieves or creates a new session token for the specified feature flag profile. + /// + /// The feature flag profile for which to get a session token. + /// A task that represents the asynchronous operation. The task result contains the session token. + /// Thrown when the provided profile is invalid. + /// + /// Session tokens are cached according to the configured cache duration to minimize API calls to AWS AppConfig. + /// private async Task GetSessionToken(FeatureFlagProfile profile) { if(!profile.IsValid) throw new ArgumentException("Invalid Feature Flag configuration profile"); @@ -140,11 +183,25 @@ private async Task GetSessionToken(FeatureFlagProfile profile) }); } + /// + /// Retrieves or creates a new session token for the specified feature flag profile. + /// + /// The feature flag profile for which to get a session token. + /// A task that represents the asynchronous operation. The task result contains the session token. + /// Thrown when the provided profile is invalid. + /// + /// Session tokens are cached according to the configured cache duration to minimize API calls to AWS AppConfig. + /// private string BuildSessionKey(FeatureFlagProfile profile) { return $"{SESSION_TOKEN_KEY_PREFIX}_{profile}"; } + /// + /// Builds a cache key for configuration values based on the feature flag profile. + /// + /// The feature flag profile to use in the key generation. + /// A unique cache key for the configuration value. private string BuildConfigurationKey(FeatureFlagProfile profile) { return $"{CONFIGURATION_VALUE_KEY_PREFIX}_{profile}"; diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/IRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/IRetrievalApi.cs new file mode 100644 index 00000000..f0621361 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/IRetrievalApi.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Amazon.AppConfigData.Model; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + /// + /// Defines the contract for interacting with AWS AppConfig Data API with caching support. + /// + public interface IRetrievalApi : IDisposable + { + /// + /// Retrieves configuration from AWS AppConfig using the provided feature flag profile. + /// Results are cached based on the configured cache duration. + /// + /// The feature flag profile containing application, environment, and configuration identifiers. + /// A task that represents the asynchronous operation. The task result contains the configuration response. + /// Thrown when unable to connect to AWS or retrieve configuration. + Task GetLatestConfigurationAsync(FeatureFlagProfile profile); + + /// + /// Invalidates the cached configuration for the specified feature flag profile. + /// + /// The feature flag profile whose configuration cache should be invalidated. + /// + /// This method forces the next GetLatestConfigurationAsync call to fetch fresh data from AWS AppConfig + /// instead of using cached values. + /// + void InvalidateConfigurationCache(FeatureFlagProfile profile); + + /// + /// Invalidates the cached session token for the specified feature flag profile. + /// + /// The feature flag profile whose session token cache should be invalidated. + /// + /// This method forces the next operation to create a new session with AWS AppConfig + /// instead of using the cached session token. + /// + void InvalidateSessionCache(FeatureFlagProfile profile); + } +} + From 1ffe6672505a8bf10fd6f1e74d981b2bb07d4d19 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Fri, 10 Jan 2025 02:10:11 +0000 Subject: [PATCH 13/43] Adding extension function for adding OpenFeature AppConfig provider Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../OpenFeatureExtension.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs new file mode 100644 index 00000000..71517155 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs @@ -0,0 +1,26 @@ +using OpenFeature; +using Microsoft.Extensions.DependencyInjection; +using Amazon.AppConfigData; + +namespace OpenFeature.Contrib.Providers.AwsAppConfig +{ + public static class OpenFeatureExtension + { + public static IServiceCollection AddAppConfigProvider(this IServiceCollection services, + string application, string environment) + { + services.AddOpenFeature(featureBuilder => { + var provider = services.BuildServiceProvider(); + var appConfigDataClient = provider.GetService(); + var appConfigRetrievalApi = new AppConfigRetrievalApi(appConfigDataClient); + + Api.Instance.SetProviderAsync(new AppConfigProvider(appConfigRetrievalApi, application, environment)); + + }); + return services; + } + + } +} + + From 668ccf82c76b0c63d0de81c837533a999b5f516c Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Fri, 10 Jan 2025 02:16:31 +0000 Subject: [PATCH 14/43] Added comments for OpenFeature extension class Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../OpenFeatureExtension.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs index 71517155..1277146d 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs @@ -4,8 +4,18 @@ namespace OpenFeature.Contrib.Providers.AwsAppConfig { + /// + /// Provides extension methods for configuring OpenFeature with AWS AppConfig integration. + /// This extension enables feature flag management using AWS AppConfig as the provider. + /// public static class OpenFeatureExtension - { + { + /// + /// Extension method for adding AppConfigProvider to the Serivce Collection. + /// + /// Name of the application for AWS AppConfig + /// Name of the environment for AWS AppConfig + /// The configured OpenFeature API instance. public static IServiceCollection AddAppConfigProvider(this IServiceCollection services, string application, string environment) { From 7c58dccfa82cb2b8036a5dc416a14d0b3a24de5b Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 02:55:04 +0000 Subject: [PATCH 15/43] adding ReadMe.md file with details about usable of the nuget package Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigKey.cs | 40 +++++++ .../OpenFeatureExtension.cs | 2 - .../README.md | 108 ++++++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs index e222e39c..7f0bb3f8 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs @@ -94,5 +94,45 @@ public AppConfigKey(string key) AttributeKey = parts[2]; } } + + /// + /// Constructs an AppConfigKey using individual components. + /// + /// The AWS AppConfig configuration profile identifier + /// The feature flag key name + /// Optional. The specific attribute key to access + /// + /// Thrown when: + /// - configurationProfileId is null, empty, or whitespace + /// - flagKey is null, empty, or whitespace + /// + public AppConfigKey(string configurationProfileId, string flagKey, string attributeKey = null) + { + if (string.IsNullOrWhiteSpace(configurationProfileId)) + { + throw new ArgumentException("Configuration Profile ID cannot be null or empty"); + } + + if (string.IsNullOrWhiteSpace(flagKey)) + { + throw new ArgumentException("Flag key cannot be null or empty"); + } + + ConfigurationProfileId = configurationProfileId; + FlagKey = flagKey; + AttributeKey = attributeKey; + } + + /// + /// Converts the AppConfigKey object back to its string representation. + /// + /// + /// A string in the format "configurationProfileId:flagKey[:attributeKey]". + /// The attributeKey part is only included if it exists. + /// + public string ToKeyString() + { + return $"{ConfigurationProfileId}{Separator}{FlagKey}{(HasAttribute ? Separator + AttributeKey : "")}"; + } } } diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs index 1277146d..31d6477d 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs @@ -32,5 +32,3 @@ public static IServiceCollection AddAppConfigProvider(this IServiceCollection se } } - - diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md new file mode 100644 index 00000000..d6857872 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -0,0 +1,108 @@ +# OpenFeature AWS AppConfig Provider + +This package provides an AWS AppConfig provider implementation for OpenFeature, allowing you to manage feature flags using AWS AppConfig. + +## Requirements + +- open-features/dotnet-sdk +- .NET Core 3.1 and above +- AWSSDK.AppConfigData for talking to AWS AppConfig +- AWS Account and Access keys / permissions for application to run in +- Microsoft.Extensions.Caching.Memory for caching local copy of AppConfig configuration + +## Installation + +Install the package via NuGet: + +```shell +dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig +``` + +## Usage + +### Basic Setup + +AWS nuget package `AWSSDK.AppConfigData` is needed for talking to AWS AppConfig. + +```csharp +namespace OpenFeatureTestApp +{ + class Program + { + static void Main(string[] args) + { + // Create the appliation builder per the application type. Here's example from + // web application + var builder = WebApplication.CreateBuilder(args); + ... + + // Add AWS AppConfig client + builder.Services.AddAWSService(); + + // Add in-memory cache provider + builder.services.AddSingleton() + + // Add OpenFeature with AWS AppConfig provider + builder.Services.AddOpenFeature(); + + var app = builder.Build(); + + // Configure OpenFeature provider for AWS AppCOnfig + var appConfigDataClient = app.Services.GetRequiredService(); + var appConfigRetrievalApi = new AppConfigRetrievalApi(appConfigDataClient); + + // Replace these values with your AWS AppConfig settings + const string application = "YourApplication"; + const string environment = "YourEnvironment"; + + await Api.Instance.SetProviderAsync( + new AppConfigProvider(appConfigRetrievalApi, application, environment) + ); + } + } +} +``` + +### Example Usage + +#### Example endpoints using feature flags + +```csharp +// Example endpoints using feature flags +app.MapGet("/feature-status", async (IFeatureClient featureClient) => +{ + var key = new AppConfigKey(configurationProfileId, flagKey, attributeName); + var isEnabled = await featureClient.GetBooleanValue(key.ToKeyString(), false); + return Results.Ok(new { FeatureEnabled = isEnabled }); +}) +.WithName("GetFeatureStatus") +.WithOpenApi(); + +app.MapGet("/feature-config", async (IFeatureClient featureClient) => +{ + var key = new AppConfigKey(configurationProfileId, flagKey, attributeName); + var config = await featureClient.GetStringValue(key.ToKeyString(), "default"); + return Results.Ok(new { Configuration = config }); +}) +.WithName("GetFeatureConfig") +.WithOpenApi(); +``` + +#### Example endpoint with feature flag controlling behavior + +```csharp +// Example endpoint with feature flag controlling behavior +app.MapGet("/protected-feature", async (IFeatureClient featureClient) => +{ + var isFeatureEnabled = await featureClient.GetBooleanValue("protected-feature", false); + + if (!isFeatureEnabled) + { + return Results.NotFound(new { Message = "Feature not available" }); + } + + return Results.Ok(new { Message = "Feature is enabled!" }); +}) +.WithName("ProtectedFeature") +.WithOpenApi(); +``` \ No newline at end of file From 8fbea1a260fb2734ce2b6ac80a8da68ed9ea8802 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:58:38 +0000 Subject: [PATCH 16/43] mermaid in Readme Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index d6857872..c1eaaeff 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -7,7 +7,7 @@ This package provides an AWS AppConfig provider implementation for OpenFeature, - open-features/dotnet-sdk - .NET Core 3.1 and above - AWSSDK.AppConfigData for talking to AWS AppConfig -- AWS Account and Access keys / permissions for application to run in +- AWS Account and Access keys / permissions for AWS AppConfig to work with - Microsoft.Extensions.Caching.Memory for caching local copy of AppConfig configuration ## Installation @@ -18,6 +18,16 @@ Install the package via NuGet: dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ``` +## Config Key +A very important stuff to understand here is the way AWS Appconfig structure is designed. + +graph TD; % This defines the direction of the flowchart +A[This is a box] --> B[Another box]; % Creates boxes with text +B --> C{Decision}; % Creates a decision node +C -->|Option 1| D[Result 1]; % Connects options to results +C -->|Option 2| E[Result 2]; + + ## Usage ### Basic Setup From c4951b524b5f3366fd9c4f3700b4ea7939c8cb0e Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:03:17 -0500 Subject: [PATCH 17/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index c1eaaeff..712f18e0 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -21,12 +21,11 @@ dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ## Config Key A very important stuff to understand here is the way AWS Appconfig structure is designed. -graph TD; % This defines the direction of the flowchart -A[This is a box] --> B[Another box]; % Creates boxes with text -B --> C{Decision}; % Creates a decision node -C -->|Option 1| D[Result 1]; % Connects options to results -C -->|Option 2| E[Result 2]; - +Application +└── Environment + └── ConfigurationProfileId + └── FeatureFlag + └── Attribute ## Usage @@ -115,4 +114,4 @@ app.MapGet("/protected-feature", async (IFeatureClient featureClient) => }) .WithName("ProtectedFeature") .WithOpenApi(); -``` \ No newline at end of file +``` From 96b5ee48dfbe5b80838089848afa42a02bcd2a7d Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:03:51 -0500 Subject: [PATCH 18/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 712f18e0..842e85cc 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -22,10 +22,10 @@ dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig A very important stuff to understand here is the way AWS Appconfig structure is designed. Application -└── Environment - └── ConfigurationProfileId - └── FeatureFlag - └── Attribute + Environment + ConfigurationProfileId + FeatureFlag + Attribute ## Usage From 0be3b5a5de6c8811ee6d463ed46f0aedf50dd7bd Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:07:29 -0500 Subject: [PATCH 19/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../README.md | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 842e85cc..9c0e0cfc 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -21,11 +21,24 @@ dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ## Config Key A very important stuff to understand here is the way AWS Appconfig structure is designed. -Application - Environment - ConfigurationProfileId - FeatureFlag - Attribute + +### Description of Each Level + +- **Application**: The top-level entity representing the application. + +- **Environment**: Different stages of deployment (e.g., Development, Staging, Production). + +- **ConfigurationProfileId**: Specific configuration profiles that group related feature flags. + +- **FeatureFlag**: Toggles that control the availability of specific features within the application. + +- **Attribute**: Additional properties associated with each feature flag (e.g., enabled status, description). + +## Example Representation + +Here’s an example representation of how these entities might look in practice: + + ## Usage From d0c38fa2b229a2fd742ca016a6d7f565ad9d2dcc Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:09:18 -0500 Subject: [PATCH 20/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 9c0e0cfc..35184f9d 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -21,6 +21,11 @@ dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ## Config Key A very important stuff to understand here is the way AWS Appconfig structure is designed. +Application +└── Environment + └── ConfigurationProfileId + └── FeatureFlag + └── Attribute [optional] ### Description of Each Level From 648668c0ff2ade9b23a0340b35437af9191859b7 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:09:51 -0500 Subject: [PATCH 21/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 35184f9d..6329173b 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -21,11 +21,13 @@ dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ## Config Key A very important stuff to understand here is the way AWS Appconfig structure is designed. +``` Application └── Environment └── ConfigurationProfileId └── FeatureFlag └── Attribute [optional] +``` ### Description of Each Level From 1b4d1489130a6933c557ee7b2f8ee54d417881fc Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:21:12 -0500 Subject: [PATCH 22/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 6329173b..ddfbcd33 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -19,14 +19,14 @@ dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ``` ## Config Key -A very important stuff to understand here is the way AWS Appconfig structure is designed. +Understanding the organization of the AWS AppConfig structure is essential. The Application serves as the top-level entity, with all other components defined underneath it, as outlined below. To obtain a feature flag value, the AppConfig client needs three elements: Application, Environment, and ConfigurationProfileId. This will return a JSON representation containing all feature flags associated with the specified ConfigurationProfileId. These flags can then be further filtered using additional values for FlagKey and attributeKey. Within the FeatureFlag, there is a default attribute named "enabled," which indicates whether the flag is active. Additional attributes can be added as needed. ``` Application └── Environment └── ConfigurationProfileId - └── FeatureFlag - └── Attribute [optional] + └── FlagKey + └── AttributeKey ``` ### Description of Each Level @@ -37,16 +37,15 @@ Application - **ConfigurationProfileId**: Specific configuration profiles that group related feature flags. -- **FeatureFlag**: Toggles that control the availability of specific features within the application. +- **FlagKey**: Toggles that control the availability of specific features within the application. -- **Attribute**: Additional properties associated with each feature flag (e.g., enabled status, description). +- **AttributeKey**: Additional properties associated with each feature flag (e.g., enabled status, description). ## Example Representation Here’s an example representation of how these entities might look in practice: - ## Usage ### Basic Setup From ce982c9457095488996bb7eab7c8addbb425dae9 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:33:44 +0000 Subject: [PATCH 23/43] Updating Readme Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigProvider.cs | 25 +------------------ .../README.md | 12 ++++++--- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs index b86b8e5e..210edc99 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs @@ -202,30 +202,7 @@ private async Task GetFeatureFlagsStreamAsync(st ConfigurationProfileIdentifier = configurationProfileId }; - return await _appConfigRetrievalApi.GetLatestConfigurationAsync(profile); - // // TODO: Yet to figure out how to pass along Evalutaion Context to AWS AppConfig - - // // Build "StartConfigurationSession" Request - // var startConfigSessionRequest = new StartConfigurationSessionRequest - // { - // ApplicationIdentifier = _applicationName, - // EnvironmentIdentifier = _environmentName, - // ConfigurationProfileIdentifier = _configurationProfileId - // }; - - // // Start a configuration session with AWS AppConfig - // var sessionResponse = await _appConfigClient.StartConfigurationSessionAsync(startConfigSessionRequest); - - // // Build "GetLatestConfiguration" request - // var configurationRequest = new GetLatestConfigurationRequest - // { - // ConfigurationToken = sessionResponse.InitialConfigurationToken - // }; - - // // Get the configuration response from AWS AppConfig - // var response = await _appConfigClient.GetLatestConfigurationAsync(configurationRequest); - - // return response; + return await _appConfigRetrievalApi.GetLatestConfigurationAsync(profile); } } } diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index ddfbcd33..a65cf17f 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -18,7 +18,7 @@ Install the package via NuGet: dotnet add package OpenFeature.Contrib.Providers.AwsAppConfig ``` -## Config Key +## AWS AppConfig Key Understanding the organization of the AWS AppConfig structure is essential. The Application serves as the top-level entity, with all other components defined underneath it, as outlined below. To obtain a feature flag value, the AppConfig client needs three elements: Application, Environment, and ConfigurationProfileId. This will return a JSON representation containing all feature flags associated with the specified ConfigurationProfileId. These flags can then be further filtered using additional values for FlagKey and attributeKey. Within the FeatureFlag, there is a default attribute named "enabled," which indicates whether the flag is active. Additional attributes can be added as needed. ``` @@ -41,9 +41,15 @@ Application - **AttributeKey**: Additional properties associated with each feature flag (e.g., enabled status, description). -## Example Representation +### Representation -Here’s an example representation of how these entities might look in practice: +This package maintains the aforementioned structure by supplying values in two distinct stages. + +Stage 1: Setup +During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain constant throughout the application's lifetime. If a change is necessary, a restart of the application will be required. + +Stage 2: Fetching Value +In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. ## Usage From cf249643083fe61ea1b32de4b2bd9a4340a69f7e Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:35:21 -0500 Subject: [PATCH 24/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index a65cf17f..3647e769 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -45,10 +45,10 @@ Application This package maintains the aforementioned structure by supplying values in two distinct stages. -Stage 1: Setup +**Stage 1: Setup** During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain constant throughout the application's lifetime. If a change is necessary, a restart of the application will be required. -Stage 2: Fetching Value +**Stage 2: Fetching Value** In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. From 2c7169569dcfd93b7f7b2f0d97f25c33724ef135 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:35:59 -0500 Subject: [PATCH 25/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 3647e769..147401ae 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -45,10 +45,10 @@ Application This package maintains the aforementioned structure by supplying values in two distinct stages. -**Stage 1: Setup** +- **Stage 1: Setup** During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain constant throughout the application's lifetime. If a change is necessary, a restart of the application will be required. -**Stage 2: Fetching Value** +- **Stage 2: Fetching Value** In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. From c7af8079d500333e7cadc4712efe3c9caee4635e Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:36:20 -0500 Subject: [PATCH 26/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 147401ae..6bd88eb7 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -46,9 +46,11 @@ Application This package maintains the aforementioned structure by supplying values in two distinct stages. - **Stage 1: Setup** + During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain constant throughout the application's lifetime. If a change is necessary, a restart of the application will be required. - **Stage 2: Fetching Value** + In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. From 44e459757a102fc9073ee12ccd01971c1baea8a2 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:36:43 -0500 Subject: [PATCH 27/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 6bd88eb7..74a116d3 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -45,11 +45,11 @@ Application This package maintains the aforementioned structure by supplying values in two distinct stages. -- **Stage 1: Setup** +**Stage 1: Setup** During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain constant throughout the application's lifetime. If a change is necessary, a restart of the application will be required. -- **Stage 2: Fetching Value** +**Stage 2: Fetching Value** In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. From 3f626f7c0deefa4fb3ee524af1740b9591dc97ab Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:44:54 -0500 Subject: [PATCH 28/43] Update README.md Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 74a116d3..50835c74 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -105,18 +105,20 @@ namespace OpenFeatureTestApp ```csharp // Example endpoints using feature flags -app.MapGet("/feature-status", async (IFeatureClient featureClient) => +app.MapGet("/flagKey", async (IFeatureClient featureClient) => { - var key = new AppConfigKey(configurationProfileId, flagKey, attributeName); + // NOTE: Refere AppConfig Key section above to understand how AppConfig configuration is strucutred. + var key = new AppConfigKey(configurationProfileId, flagKey, "enabled"); var isEnabled = await featureClient.GetBooleanValue(key.ToKeyString(), false); return Results.Ok(new { FeatureEnabled = isEnabled }); }) .WithName("GetFeatureStatus") .WithOpenApi(); -app.MapGet("/feature-config", async (IFeatureClient featureClient) => +app.MapGet("/flagKey/attributeKey", async (IFeatureClient featureClient) => { - var key = new AppConfigKey(configurationProfileId, flagKey, attributeName); + // NOTE: Refere AppConfig Key section above to understand how AppConfig configuration is strucutred. + var key = new AppConfigKey(configurationProfileId, flagKey, attributeKey); var config = await featureClient.GetStringValue(key.ToKeyString(), "default"); return Results.Ok(new { Configuration = config }); }) @@ -130,7 +132,8 @@ app.MapGet("/feature-config", async (IFeatureClient featureClient) => // Example endpoint with feature flag controlling behavior app.MapGet("/protected-feature", async (IFeatureClient featureClient) => { - var isFeatureEnabled = await featureClient.GetBooleanValue("protected-feature", false); + var key = new AppConfigKey(configurationProfileId, "protected-feature", "enabled"); + var isFeatureEnabled = await featureClient.GetBooleanValue(key.ToKeyString(), false); if (!isFeatureEnabled) { From 93a141e1f553387d6113c263eecd2d8f17f03b14 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 16 Jan 2025 03:51:41 +0000 Subject: [PATCH 29/43] adding tests Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigProvider.cs | 1 - .../FeatureFlagProfile.cs | 2 - ...ture.Contrib.Providers.AwsAppConfig.csproj | 2 +- .../OpenFeatureExtension.cs | 34 ------ .../AppConfigKeyTests.cs | 110 ++++++++++++++++++ .../Class1.cs | 6 - .../FeatureFlagProfileTests.cs | 91 +++++++++++++++ ...Contrib.Providers.AwsAppConfig.Test.csproj | 11 +- 8 files changed, 207 insertions(+), 50 deletions(-) delete mode 100644 src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs delete mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagProfileTests.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs index 210edc99..79900ae5 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; using OpenFeature.Model; -using Amazon.AppConfigData; using Amazon.AppConfigData.Model; namespace OpenFeature.Contrib.Providers.AwsAppConfig diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs index c28c126b..1a6b2ac2 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs @@ -1,5 +1,3 @@ -using System; - namespace OpenFeature.Contrib.Providers.AwsAppConfig { /// diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj index 42186d4d..d3b11b87 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj @@ -1,7 +1,7 @@  - net8.0;net7.0;net6.0;netcoreapp3.1; + net8.0;net7.0;net6.0; 7.3 OpenFeature.Contrib.Providers.AwsAppConfig 0.0.1 diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs deleted file mode 100644 index 31d6477d..00000000 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeatureExtension.cs +++ /dev/null @@ -1,34 +0,0 @@ -using OpenFeature; -using Microsoft.Extensions.DependencyInjection; -using Amazon.AppConfigData; - -namespace OpenFeature.Contrib.Providers.AwsAppConfig -{ - /// - /// Provides extension methods for configuring OpenFeature with AWS AppConfig integration. - /// This extension enables feature flag management using AWS AppConfig as the provider. - /// - public static class OpenFeatureExtension - { - /// - /// Extension method for adding AppConfigProvider to the Serivce Collection. - /// - /// Name of the application for AWS AppConfig - /// Name of the environment for AWS AppConfig - /// The configured OpenFeature API instance. - public static IServiceCollection AddAppConfigProvider(this IServiceCollection services, - string application, string environment) - { - services.AddOpenFeature(featureBuilder => { - var provider = services.BuildServiceProvider(); - var appConfigDataClient = provider.GetService(); - var appConfigRetrievalApi = new AppConfigRetrievalApi(appConfigDataClient); - - Api.Instance.SetProviderAsync(new AppConfigProvider(appConfigRetrievalApi, application, environment)); - - }); - return services; - } - - } -} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs new file mode 100644 index 00000000..b33f5c94 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs @@ -0,0 +1,110 @@ +using Xunit; +using OpenFeature.Contrib.Providers.AwsAppConfig; +using System; + +public class AppConfigKeyTests +{ + [Fact] + public void Constructor_WithValidParameters_ShouldSetProperties() + { + // Arrange + var configProfileID = "TestConfigProfile"; + var flagKey = "TestFlagKey"; + var attributeKey = "TestAttributeKey"; + + // Act + var key = new AppConfigKey(configProfileID, flagKey, attributeKey); + + // Assert + Assert.Equal(configProfileID, key.ConfigurationProfileId); + Assert.Equal(flagKey, key.FlagKey); + Assert.Equal(attributeKey, key.AttributeKey); + } + + [Theory] + [InlineData("", "env", "config")] + [InlineData("app", "", "config")] + [InlineData("app", "env", "")] + [InlineData(null, "env", "config")] + [InlineData("app", null, "config")] + [InlineData("app", "env", null)] + public void Constructor_WithInvalidParameters_ShouldThrowArgumentException( + string application, string environment, string configuration) + { + // Act & Assert + Assert.Throws(() => + new AppConfigKey(application, environment, configuration)); + } + + [Theory] + [InlineData("app1", "env1", "config1")] + [InlineData("my-app", "my-env", "my-config")] + [InlineData("APP", "ENV", "CONFIG")] + public void ToString_ShouldReturnFormattedString( + string configProfileId, string flagKey, string attributeKey) + { + // Arrange + var key = new AppConfigKey(configProfileId, flagKey, attributeKey); + + // Act + var result = key.ToString(); + + // Assert + Assert.Contains(configProfileId, result); + Assert.Contains(flagKey, result); + Assert.Contains(attributeKey, result); + } + + [Theory] + [InlineData("app-123", "env-123", "config-123")] + [InlineData("app_123", "env_123", "config_123")] + [InlineData("app.123", "env.123", "config.123")] + public void Constructor_WithSpecialCharacters_ShouldAcceptValidPatterns( + string configProfileId, string flagKey, string attributeKey) + { + // Arrange & Act + var key = new AppConfigKey(configProfileId, flagKey, attributeKey); + + // Assert + Assert.Equal(configProfileId, key.ConfigurationProfileId); + Assert.Equal(flagKey, key.FlagKey); + Assert.Equal(attributeKey, key.AttributeKey); + } + + [Theory] + [InlineData("app$123", "env", "config")] + [InlineData("app", "env#123", "config")] + [InlineData("app", "env", "config@123")] + public void Constructor_WithInvalidCharacters_ShouldThrowArgumentException( + string application, string environment, string configuration) + { + // Act & Assert + Assert.Throws(() => + new AppConfigKey(application, environment, configuration)); + } + + [Fact] + public void Constructor_WithWhitespaceValues_ShouldThrowArgumentException() + { + // Arrange + var application = " "; + var environment = "env"; + var configuration = "config"; + + // Act & Assert + Assert.Throws(() => + new AppConfigKey(application, environment, configuration)); + } + + [Theory] + [InlineData("a", "env", "config")] // too short + [InlineData("app", "e", "config")] // too short + [InlineData("app", "env", "c")] // too short + public void Constructor_WithTooShortValues_ShouldThrowArgumentException( + string application, string environment, string configuration) + { + // Act & Assert + Assert.Throws(() => + new AppConfigKey(application, environment, configuration)); + } +} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs deleted file mode 100644 index 18ad82dc..00000000 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenFeature.Contrib.Providers.AwsAppConfig.Test; - -public class Class1 -{ - -} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagProfileTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagProfileTests.cs new file mode 100644 index 00000000..af871aeb --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagProfileTests.cs @@ -0,0 +1,91 @@ +using Xunit; +using OpenFeature.Contrib.Providers.AwsAppConfig; + +public class FeatureFlagProfileTests +{ + [Fact] + public void Constructor_ShouldInitializeWithDefaultValues() + { + // Arrange & Act + var profile = new FeatureFlagProfile(); + + // Assert + Assert.NotNull(profile); + // Add assertions for any default properties that should be initialized + } + + [Fact] + public void PropertySetter_ShouldSetProperties() + { + // Arrange + var appname = "TestApplication"; + var environment = "Test Environment"; + var configProfileId = "Test Configuration"; + + // Act + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = appname, + EnvironmentIdentifier = environment, + ConfigurationProfileIdentifier = configProfileId, + + }; + + // Assert + Assert.Equal(appname, profile.ApplicationIdentifier); + Assert.Equal(environment, profile.EnvironmentIdentifier); + Assert.Equal(configProfileId, profile.ConfigurationProfileIdentifier); + } + + [Theory] + [InlineData("TestApplication", "TestEnvironment", "TestConfigProfileId")] + [InlineData("Test2Application", "Test2Environment", "Test2ConfigProfileId")] + public void ToString_ShouldReturnKeyString(string appName, string env, string configProfileId) + { + // Arrange + var profile = new FeatureFlagProfile { + ApplicationIdentifier = appName, + EnvironmentIdentifier = env, + ConfigurationProfileIdentifier = configProfileId, + }; + + // Act + var result = profile.ToString(); + + // Assert + Assert.Equal($"{appName}_{env}_{configProfileId}", result); + } + + [Theory] + [InlineData("TestApplication", "TestEnvironment", "TestConfigProfileId")] + [InlineData("Test2Application", "Test2Environment", "Test2ConfigProfileId")] + public void IsValid_ReturnTrue(string appName, string env, string configProfileId) + { + // Arrange + var profile = new FeatureFlagProfile { + ApplicationIdentifier = appName, + EnvironmentIdentifier = env, + ConfigurationProfileIdentifier = configProfileId, + }; + + // Assert + Assert.True(profile.IsValid); + } + + [Theory] + [InlineData("", "TestEnvironment", "TestConfigProfileId")] + [InlineData("TestApplication", "", "TestConfigProfileId")] + [InlineData("TestApplication", "TestEnvironment", "")] + [InlineData("", "", "")] + public void IsValid_ReturnFalse(string appName, string env, string configProfileId) + { + // Arrange + var profile = new FeatureFlagProfile { + ApplicationIdentifier = appName, + EnvironmentIdentifier = env, + ConfigurationProfileIdentifier = configProfileId, + }; + + // Assert + Assert.False(profile.IsValid); + } +} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj index 24ed1d88..b40cde2d 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj @@ -1,13 +1,12 @@  - - + + - - net8.0 - enable - enable + + false + true From 5aa4875a9a3bbe7c14b5177b34174d18aaaa741a Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:19:10 +0000 Subject: [PATCH 30/43] added more tests Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigKey.cs | 11 +- .../AppConfigKeyTests.cs | 220 +++++++++++++++--- 2 files changed, 198 insertions(+), 33 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs index 7f0bb3f8..493f302f 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs @@ -1,4 +1,5 @@ using System; +using System.Text.RegularExpressions; namespace OpenFeature.Contrib.Providers.AwsAppConfig { @@ -31,7 +32,7 @@ public class AppConfigKey /// /// Gets whether this key has an attribute component /// - public bool HasAttribute => !string.IsNullOrEmpty(AttributeKey); + public bool HasAttribute => !string.IsNullOrWhiteSpace(AttributeKey); /// /// Initializes a new instance of the class that represents a structured key @@ -73,6 +74,10 @@ public class AppConfigKey /// public AppConfigKey(string key) { + // Regular expression for validating the format with last part as 0 or 1 or empty + string pattern = @"^[^:]+:[^:]+:(0|1)?$"; + var match = Regex.IsMatch(key, pattern); + if(string.IsNullOrWhiteSpace(key)) { throw new ArgumentException("Key cannot be null or empty"); @@ -110,12 +115,12 @@ public AppConfigKey(string configurationProfileId, string flagKey, string attrib { if (string.IsNullOrWhiteSpace(configurationProfileId)) { - throw new ArgumentException("Configuration Profile ID cannot be null or empty"); + throw new ArgumentNullException("Configuration Profile ID cannot be null or empty"); } if (string.IsNullOrWhiteSpace(flagKey)) { - throw new ArgumentException("Flag key cannot be null or empty"); + throw new ArgumentNullException("Flag key cannot be null or empty"); } ConfigurationProfileId = configurationProfileId; diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs index b33f5c94..8c55027d 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs @@ -5,7 +5,7 @@ public class AppConfigKeyTests { [Fact] - public void Constructor_WithValidParameters_ShouldSetProperties() + public void Constructor_3input_WithValidParameters_ShouldSetProperties() { // Arrange var configProfileID = "TestConfigProfile"; @@ -23,17 +23,15 @@ public void Constructor_WithValidParameters_ShouldSetProperties() [Theory] [InlineData("", "env", "config")] - [InlineData("app", "", "config")] - [InlineData("app", "env", "")] + [InlineData("app", "", "config")] [InlineData(null, "env", "config")] - [InlineData("app", null, "config")] - [InlineData("app", "env", null)] - public void Constructor_WithInvalidParameters_ShouldThrowArgumentException( - string application, string environment, string configuration) + [InlineData("app", null, "config")] + public void Constructor_3input_WithInvalidParameters_ShouldThrowArgumentException( + string confiProfileId, string flagKey, string attributeKey) { // Act & Assert Assert.Throws(() => - new AppConfigKey(application, environment, configuration)); + new AppConfigKey(confiProfileId, flagKey, attributeKey)); } [Theory] @@ -59,7 +57,7 @@ public void ToString_ShouldReturnFormattedString( [InlineData("app-123", "env-123", "config-123")] [InlineData("app_123", "env_123", "config_123")] [InlineData("app.123", "env.123", "config.123")] - public void Constructor_WithSpecialCharacters_ShouldAcceptValidPatterns( + public void Constructor_3input_WithSpecialCharacters_ShouldAcceptValidPatterns( string configProfileId, string flagKey, string attributeKey) { // Arrange & Act @@ -69,42 +67,204 @@ public void Constructor_WithSpecialCharacters_ShouldAcceptValidPatterns( Assert.Equal(configProfileId, key.ConfigurationProfileId); Assert.Equal(flagKey, key.FlagKey); Assert.Equal(attributeKey, key.AttributeKey); - } + } - [Theory] - [InlineData("app$123", "env", "config")] - [InlineData("app", "env#123", "config")] - [InlineData("app", "env", "config@123")] - public void Constructor_WithInvalidCharacters_ShouldThrowArgumentException( - string application, string environment, string configuration) + [Fact] + public void Constructor_3input_WithWhitespaceValues_ShouldThrowArgumentException() { + // Arrange + var application = " "; + var environment = "env"; + var configuration = "config"; + // Act & Assert - Assert.Throws(() => + Assert.Throws(() => new AppConfigKey(application, environment, configuration)); } [Fact] - public void Constructor_WithWhitespaceValues_ShouldThrowArgumentException() + public void Constructor_WithNullKey_ThrowsArgumentException() + { + // Act & Assert + var exception = Assert.Throws(() => new AppConfigKey(null)); + Assert.Equal("Key cannot be null or empty", exception.Message); + } + + [Fact] + public void Constructor_WithEmptyKey_ThrowsArgumentException() + { + // Act & Assert + var exception = Assert.Throws(() => new AppConfigKey(string.Empty)); + Assert.Equal("Key cannot be null or empty", exception.Message); + } + + [Fact] + public void Constructor_WithWhitespaceKey_ThrowsArgumentException() + { + // Act & Assert + var exception = Assert.Throws(() => new AppConfigKey(" ")); + Assert.Equal("Key cannot be null or empty", exception.Message); + } + + [Fact] + public void Constructor_WithSinglePart_ThrowsArgumentException() + { + // Act & Assert + var exception = Assert.Throws(() => new AppConfigKey("singlepart")); + Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); + } + + [Fact] + public void Constructor_WithTwoParts_SetsPropertiesCorrectly() { // Arrange - var application = " "; - var environment = "env"; - var configuration = "config"; + var key = "profile123:flag456"; + + // Act + var appConfigKey = new AppConfigKey(key); + + // Assert + Assert.Equal("profile123", appConfigKey.ConfigurationProfileId); + Assert.Equal("flag456", appConfigKey.FlagKey); + Assert.Null(appConfigKey.AttributeKey); + Assert.False(appConfigKey.HasAttribute); + } + [Fact] + public void Constructor_WithThreeParts_SetsPropertiesCorrectly() + { + // Arrange + var key = "profile123:flag456:attr789"; + + // Act + var appConfigKey = new AppConfigKey(key); + + // Assert + Assert.Equal("profile123", appConfigKey.ConfigurationProfileId); + Assert.Equal("flag456", appConfigKey.FlagKey); + Assert.Equal("attr789", appConfigKey.AttributeKey); + Assert.True(appConfigKey.HasAttribute); + } + + [Fact] + public void Constructor_WithMoreThanThreeParts_IgnoresExtraParts() + { + // Arrange + var key = "profile123:flag456:attr789:extra:parts"; + + // Act + var appConfigKey = new AppConfigKey(key); + + // Assert + Assert.Equal("profile123", appConfigKey.ConfigurationProfileId); + Assert.Equal("flag456", appConfigKey.FlagKey); + Assert.Equal("attr789", appConfigKey.AttributeKey); + Assert.True(appConfigKey.HasAttribute); + } + + [Fact] + public void Constructor_WithEmptyParts_ThrowsArgumentException() + { // Act & Assert - Assert.Throws(() => - new AppConfigKey(application, environment, configuration)); + var exception = Assert.Throws(() => new AppConfigKey("::")); + Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); } [Theory] - [InlineData("a", "env", "config")] // too short - [InlineData("app", "e", "config")] // too short - [InlineData("app", "env", "c")] // too short - public void Constructor_WithTooShortValues_ShouldThrowArgumentException( - string application, string environment, string configuration) + [InlineData("profile123::attr789")] + [InlineData(":flag456:attr789")] + [InlineData("::attr789")] + public void Constructor_WithEmptyMiddleParts_PreservesNonEmptyParts(string key) { // Act & Assert - Assert.Throws(() => - new AppConfigKey(application, environment, configuration)); + var exception = Assert.Throws(() => new AppConfigKey(key)); + Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); + } + + [Fact] + public void Constructor_WithTrailingSeparator_HandlesProperly() + { + // Arrange + var key = "profile123:flag456:"; + + // Act + var appConfigKey = new AppConfigKey(key); + + // Assert + Assert.Equal("profile123", appConfigKey.ConfigurationProfileId); + Assert.Equal("flag456", appConfigKey.FlagKey); + Assert.Null(appConfigKey.AttributeKey); + Assert.False(appConfigKey.HasAttribute); + } + + [Fact] + public void Constructor_WithLeadingSeparator_ThrowsArgumentException() + { + // Arrange + var key = ":profile123:flag456"; + + // Act & Assert + var exception = Assert.Throws(() => new AppConfigKey(key)); + Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); + } + + [Fact] + public void HasAttribute_WhenAttributeKeyIsNull_ReturnsFalse() + { + // Arrange + var appConfigKey = new AppConfigKey("profileId", "flagKey"); + + // Act & Assert + Assert.False(appConfigKey.HasAttribute); + } + + [Fact] + public void HasAttribute_WhenAttributeKeyIsEmpty_ReturnsFalse() + { + // Arrange + var appConfigKey = new AppConfigKey("profileId", "flagKey", ""); + + // Act & Assert + Assert.False(appConfigKey.HasAttribute); + } + + [Fact] + public void HasAttribute_WhenAttributeKeyIsWhitespace_ReturnsFalse() + { + // Arrange + var appConfigKey = new AppConfigKey("profileId", "flagKey", " "); + + // Act & Assert + Assert.False(appConfigKey.HasAttribute); + } + + [Fact] + public void HasAttribute_WhenAttributeKeyIsProvided_ReturnsTrue() + { + // Arrange + var appConfigKey = new AppConfigKey("profileId", "flagKey", "attributeKey"); + + // Act & Assert + Assert.True(appConfigKey.HasAttribute); + } + + [Fact] + public void HasAttribute_WhenConstructedWithStringWithAttribute_ReturnsTrue() + { + // Arrange + var appConfigKey = new AppConfigKey("profileId:flagKey:attributeKey"); + + // Act & Assert + Assert.True(appConfigKey.HasAttribute); + } + + [Fact] + public void HasAttribute_WhenConstructedWithStringWithoutAttribute_ReturnsFalse() + { + // Arrange + var appConfigKey = new AppConfigKey("profileId:flagKey"); + + // Act & Assert + Assert.False(appConfigKey.HasAttribute); } } From ac3cfd6eb11fa1c26f2d66f6e0b2f838d34dfd2f Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:30:20 +0000 Subject: [PATCH 31/43] added more tests Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigKey.cs | 13 ++- .../README.md | 6 +- .../AppConfigKeyTests.cs | 107 +++++++++--------- 3 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs index 493f302f..097508c0 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs @@ -74,15 +74,18 @@ public class AppConfigKey /// public AppConfigKey(string key) { - // Regular expression for validating the format with last part as 0 or 1 or empty - string pattern = @"^[^:]+:[^:]+:(0|1)?$"; - var match = Regex.IsMatch(key, pattern); - if(string.IsNullOrWhiteSpace(key)) { throw new ArgumentException("Key cannot be null or empty"); } + // Regular expression for validating the format with last part as 0 or 1 or empty + string pattern = @"^[^:]+:[^:]+(:[^:]+)?$"; + var match = Regex.IsMatch(key, pattern); + + if(!match) throw new ArgumentException("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format"); + + var parts = key.Split(Separator, StringSplitOptions.RemoveEmptyEntries); if(parts.Length < 2 ) @@ -135,7 +138,7 @@ public AppConfigKey(string configurationProfileId, string flagKey, string attrib /// A string in the format "configurationProfileId:flagKey[:attributeKey]". /// The attributeKey part is only included if it exists. /// - public string ToKeyString() + public override string ToString() { return $"{ConfigurationProfileId}{Separator}{FlagKey}{(HasAttribute ? Separator + AttributeKey : "")}"; } diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 50835c74..65ed6de9 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -109,7 +109,7 @@ app.MapGet("/flagKey", async (IFeatureClient featureClient) => { // NOTE: Refere AppConfig Key section above to understand how AppConfig configuration is strucutred. var key = new AppConfigKey(configurationProfileId, flagKey, "enabled"); - var isEnabled = await featureClient.GetBooleanValue(key.ToKeyString(), false); + var isEnabled = await featureClient.GetBooleanValue(key.ToString(), false); return Results.Ok(new { FeatureEnabled = isEnabled }); }) .WithName("GetFeatureStatus") @@ -119,7 +119,7 @@ app.MapGet("/flagKey/attributeKey", async (IFeatureClient featureClient) => { // NOTE: Refere AppConfig Key section above to understand how AppConfig configuration is strucutred. var key = new AppConfigKey(configurationProfileId, flagKey, attributeKey); - var config = await featureClient.GetStringValue(key.ToKeyString(), "default"); + var config = await featureClient.GetStringValue(key.ToString(), "default"); return Results.Ok(new { Configuration = config }); }) .WithName("GetFeatureConfig") @@ -133,7 +133,7 @@ app.MapGet("/flagKey/attributeKey", async (IFeatureClient featureClient) => app.MapGet("/protected-feature", async (IFeatureClient featureClient) => { var key = new AppConfigKey(configurationProfileId, "protected-feature", "enabled"); - var isFeatureEnabled = await featureClient.GetBooleanValue(key.ToKeyString(), false); + var isFeatureEnabled = await featureClient.GetBooleanValue(key.ToString(), false); if (!isFeatureEnabled) { diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs index 8c55027d..b7a19e20 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs @@ -32,26 +32,7 @@ public void Constructor_3input_WithInvalidParameters_ShouldThrowArgumentExceptio // Act & Assert Assert.Throws(() => new AppConfigKey(confiProfileId, flagKey, attributeKey)); - } - - [Theory] - [InlineData("app1", "env1", "config1")] - [InlineData("my-app", "my-env", "my-config")] - [InlineData("APP", "ENV", "CONFIG")] - public void ToString_ShouldReturnFormattedString( - string configProfileId, string flagKey, string attributeKey) - { - // Arrange - var key = new AppConfigKey(configProfileId, flagKey, attributeKey); - - // Act - var result = key.ToString(); - - // Assert - Assert.Contains(configProfileId, result); - Assert.Contains(flagKey, result); - Assert.Contains(attributeKey, result); - } + } [Theory] [InlineData("app-123", "env-123", "config-123")] @@ -146,29 +127,23 @@ public void Constructor_WithThreeParts_SetsPropertiesCorrectly() Assert.True(appConfigKey.HasAttribute); } - [Fact] - public void Constructor_WithMoreThanThreeParts_IgnoresExtraParts() - { - // Arrange - var key = "profile123:flag456:attr789:extra:parts"; - - // Act - var appConfigKey = new AppConfigKey(key); - - // Assert - Assert.Equal("profile123", appConfigKey.ConfigurationProfileId); - Assert.Equal("flag456", appConfigKey.FlagKey); - Assert.Equal("attr789", appConfigKey.AttributeKey); - Assert.True(appConfigKey.HasAttribute); - } - - [Fact] - public void Constructor_WithEmptyParts_ThrowsArgumentException() + [Theory] + [InlineData("profile123:flag456:attr789:extra:parts")] + [InlineData("profile123::attr789:extra:parts")] + [InlineData("profile123:flagkey456:")] + [InlineData("profile123:")] + [InlineData(":flagkey456")] + [InlineData(":flagkey456:")] + [InlineData("::attribute789")] + [InlineData("::")] + [InlineData(":::")] + [InlineData("RandomSgring)()@*Q()*#Q$@#$")] + public void Constructor_WithInvalidPattern_ShouldThrowArgumentException(string key) { // Act & Assert var exception = Assert.Throws(() => new AppConfigKey("::")); Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); - } + } [Theory] [InlineData("profile123::attr789")] @@ -179,23 +154,7 @@ public void Constructor_WithEmptyMiddleParts_PreservesNonEmptyParts(string key) // Act & Assert var exception = Assert.Throws(() => new AppConfigKey(key)); Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); - } - - [Fact] - public void Constructor_WithTrailingSeparator_HandlesProperly() - { - // Arrange - var key = "profile123:flag456:"; - - // Act - var appConfigKey = new AppConfigKey(key); - - // Assert - Assert.Equal("profile123", appConfigKey.ConfigurationProfileId); - Assert.Equal("flag456", appConfigKey.FlagKey); - Assert.Null(appConfigKey.AttributeKey); - Assert.False(appConfigKey.HasAttribute); - } + } [Fact] public void Constructor_WithLeadingSeparator_ThrowsArgumentException() @@ -267,4 +226,40 @@ public void HasAttribute_WhenConstructedWithStringWithoutAttribute_ReturnsFalse( // Act & Assert Assert.False(appConfigKey.HasAttribute); } + + [Theory] + [InlineData("app1", "env1", "config1")] + [InlineData("my-app", "my-env", "my-config")] + [InlineData("APP", "ENV", "CONFIG")] + public void ToString_ShouldReturnFormattedString( + string configProfileId, string flagKey, string attributeKey) + { + // Arrange + var key = new AppConfigKey(configProfileId, flagKey, attributeKey); + + // Act + var result = key.ToString(); + + // Assert + Assert.Contains(configProfileId, result); + Assert.Contains(flagKey, result); + Assert.Contains(attributeKey, result); + } + + [Theory] + [InlineData("app1:env1:config1")] + [InlineData("my-app:my-env:my-config")] + [InlineData("APP:ENV")] + public void ToString_WithSingleInput_ShouldReturnFormattedString(string input) + { + // Arrange + var key = new AppConfigKey(input); + + // Act + var result = key.ToString(); + + // Assert + Assert.Contains(key.ConfigurationProfileId, result); + Assert.Contains(key.FlagKey, result); + } } From 178a15cb7cc260eb3689019642893824ca5b1db5 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Fri, 17 Jan 2025 04:22:45 +0000 Subject: [PATCH 32/43] Adding FeatureFlagParser class's unit tests Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigKeyTests.cs | 2 +- .../FeatureFlagParserTests.cs | 56 +++++++++++++++++++ ...Contrib.Providers.AwsAppConfig.Test.csproj | 10 ++++ .../test-data.json | 9 +++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs index b7a19e20..be4e8224 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs @@ -141,7 +141,7 @@ public void Constructor_WithThreeParts_SetsPropertiesCorrectly() public void Constructor_WithInvalidPattern_ShouldThrowArgumentException(string key) { // Act & Assert - var exception = Assert.Throws(() => new AppConfigKey("::")); + var exception = Assert.Throws(() => new AppConfigKey(key)); Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message); } diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs new file mode 100644 index 00000000..cb4e6435 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs @@ -0,0 +1,56 @@ +using Xunit; +using System; +using System.Text.Json; +using OpenFeature.Model; +using Microsoft.Extensions.Configuration; +using OpenFeature.Contrib.Providers.AwsAppConfig; + +public class FeatureFlagParserTests +{ + private readonly string _jsonContent; + + public FeatureFlagParserTests() + { + _jsonContent = System.IO.File.ReadAllText("test-data.json"); + } + + [Fact] + public void ParseFeatureFlag_EnabledFlag_ReturnsValue() + { + // Act + var result = FeatureFlagParser.ParseFeatureFlag("test-enabled-flag", new Value(), _jsonContent); + + // Assert + Assert.True(result.IsStructure); + Assert.True(result.AsStructure["enabled"].AsBoolean); + Assert.Equal("testValue", result.AsStructure["additionalAttribute"].AsString); + } + + [Fact] + public void ParseFeatureFlag_DisabledFlag_ReturnsValue() + { + // Act + var result = FeatureFlagParser.ParseFeatureFlag("test-disabled-flag", new Value(), _jsonContent); + + // Assert + Assert.True(result.IsStructure); + Assert.False(result.AsStructure["enabled"].AsBoolean); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("invalid")] + public void ParseFeatureFlag_WhenValueIsInvalid_ThrowsArgumentNullException(string input) + { + // Act & Assert + if(input == null){ + Assert.Throws(() => FeatureFlagParser.ParseFeatureFlag("test-enabled-flag", new Value(), input)); + } + else + { + Assert.Throws(() => FeatureFlagParser.ParseFeatureFlag("test-enabled-flag", new Value(), input)); + } + + } +} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj index b40cde2d..13bb68b9 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj @@ -4,9 +4,19 @@ + + + + false true + + + Always + + + diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json new file mode 100644 index 00000000..24b0db6d --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json @@ -0,0 +1,9 @@ +{ + "test-enabled-flag": { + "enabled": true, + "additionalAttribute": "testValue" + }, + "test-disabled-flag": { + "enabled": false + } +} From e9186a908fa853826a5b37215e6c2c1b6f2dcd9d Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:38:53 +0000 Subject: [PATCH 33/43] tests for AppConfigProvider Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigProviderTests.cs | 239 ++++++++++++++++++ .../FeatureFlagParserTests.cs | 2 +- ...Contrib.Providers.AwsAppConfig.Test.csproj | 2 + .../test-data.json | 4 +- 4 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs new file mode 100644 index 00000000..1b5e2afa --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs @@ -0,0 +1,239 @@ +using Xunit; +using Moq; +using OpenFeature.Model; +using Amazon.AppConfigData.Model; +using System.Text; +using System.IO; +using OpenFeature.Contrib.Providers.AwsAppConfig; +using System.Threading.Tasks; +using System.Collections.Generic; + +public class AppConfigProviderTests +{ + private readonly Mock _mockAppConfigApi; + private readonly AppConfigProvider _provider; + private readonly string _jsonContent; + private const string ApplicationName = "TestApp"; + private const string EnvironmentName = "TestEnv"; + + public AppConfigProviderTests() + { + _mockAppConfigApi = new Mock(); + _provider = new AppConfigProvider(_mockAppConfigApi.Object, ApplicationName, EnvironmentName); + _jsonContent = System.IO.File.ReadAllText("test-data.json"); + } + + #region ResolveBooleanValueAsync Tests + [Fact] + public async Task ResolveBooleanValueAsync_WhenFlagExists_ReturnsCorrectValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:enabled"; + const bool expectedValue = true; + SetupMockResponse(_jsonContent); + + // Act + var result = await _provider.ResolveBooleanValueAsync(flagKey, false); + + // Assert + Assert.Equal(expectedValue, result.Value); + Assert.Equal(flagKey, result.FlagKey); + } + + [Fact] + public async Task ResolveBooleanValueAsync_WhenFlagDoesNotExist_ReturnsDefaultValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:enabled"; + const bool defaultValue = false; + SetupMockResponse("{}"); + + // Act + var result = await _provider.ResolveBooleanValueAsync(flagKey, defaultValue); + + // Assert + Assert.Equal(defaultValue, result.Value); + } + #endregion + + #region ResolveDoubleValueAsync Tests + [Fact] + public async Task ResolveDoubleValueAsync_WhenFlagExists_ReturnsCorrectValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:doubleAttribute"; + const double expectedValue = 3.14; + SetupMockResponse(_jsonContent); + + // Act + var result = await _provider.ResolveDoubleValueAsync(flagKey, 0.0); + + // Assert + Assert.Equal(expectedValue, result.Value); + Assert.Equal(flagKey, result.FlagKey); + } + + [Fact] + public async Task ResolveDoubleValueAsync_WhenFlagDoesNotExist_ReturnsDefaultValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:doubleAttribute"; + const double defaultValue = 1.0; + SetupMockResponse("{}"); + + // Act + var result = await _provider.ResolveDoubleValueAsync(flagKey, defaultValue); + + // Assert + Assert.Equal(defaultValue, result.Value); + } + #endregion + + #region ResolveIntegerValueAsync Tests + [Fact] + public async Task ResolveIntegerValueAsync_WhenFlagExists_ReturnsCorrectValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:intAttribute"; + const int expectedValue = 42; + SetupMockResponse(_jsonContent); + + // Act + var result = await _provider.ResolveIntegerValueAsync(flagKey, 0); + + // Assert + Assert.Equal(expectedValue, result.Value); + Assert.Equal(flagKey, result.FlagKey); + } + + [Fact] + public async Task ResolveIntegerValueAsync_WhenFlagDoesNotExist_ReturnsDefaultValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:intAttribute"; + const int defaultValue = 100; + SetupMockResponse("{}"); + + // Act + var result = await _provider.ResolveIntegerValueAsync(flagKey, defaultValue); + + // Assert + Assert.Equal(defaultValue, result.Value); + } + #endregion + + #region ResolveStringValueAsync Tests + [Fact] + public async Task ResolveStringValueAsync_WhenFlagExists_ReturnsCorrectValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:stringAttribute"; + const string expectedValue = "testValue"; + SetupMockResponse(_jsonContent); + + // Act + var result = await _provider.ResolveStringValueAsync(flagKey, "default"); + + // Assert + Assert.Equal(expectedValue, result.Value); + Assert.Equal(flagKey, result.FlagKey); + } + + [Fact] + public async Task ResolveStringValueAsync_WhenFlagDoesNotExist_ReturnsDefaultValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag:stringAttribute"; + const string defaultValue = "default-value"; + SetupMockResponse("{}"); + + // Act + var result = await _provider.ResolveStringValueAsync(flagKey, defaultValue); + + // Assert + Assert.Equal(defaultValue, result.Value); + } + #endregion + + #region ResolveStructureValueAsync Tests + [Fact] + public async Task ResolveStructureValueAsync_WhenFlagExists_ReturnsCorrectValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag"; + const string jsonValue = "{\"key\": \"value\", \"number\": 42}"; + SetupMockResponse($"{{\"{flagKey}\": {jsonValue}}}"); + + // Act + var result = await _provider.ResolveStructureValueAsync(flagKey, new Value()); + + // Assert + Assert.NotNull(result.Value.AsStructure); + Assert.Equal("value", result.Value.AsStructure["key"].AsString); + Assert.Equal(42, result.Value.AsStructure["number"].AsInteger); + } + + [Fact] + public async Task ResolveStructureValueAsync_WhenFlagDoesNotExist_ReturnsDefaultValue() + { + // Arrange + const string flagKey = "configProfileId:test-enabled-flag"; + var defaultValue = new Value(new Dictionary + { + ["default"] = new Value("default") + }); + SetupMockResponse("{}"); + + // Act + var result = await _provider.ResolveStructureValueAsync(flagKey, defaultValue); + + // Assert + Assert.Equal(defaultValue.AsStructure["default"].AsString, + result.Value.AsStructure["default"].AsString); + } + #endregion + + #region Attribute Resolution Tests + [Fact] + public async Task ResolveValue_WithAttributeKey_ReturnsAttributeValue() + { + // Arrange + const string flagKey = "myFlag:color"; + const string expectedValue = "blue"; + SetupMockResponse($"{{\"myFlag\": {{\"color\": \"{expectedValue}\"}}}}"); + + // Act + var result = await _provider.ResolveStringValueAsync(flagKey, "default"); + + // Assert + Assert.Equal(expectedValue, result.Value); + } + + [Fact] + public async Task ResolveValue_WithInvalidAttributeKey_ReturnsDefaultValue() + { + // Arrange + const string flagKey = "myFlag:invalidAttribute"; + const string defaultValue = "default"; + SetupMockResponse("{\"myFlag\": {\"color\": \"blue\"}}"); + + // Act + var result = await _provider.ResolveStringValueAsync(flagKey, defaultValue); + + // Assert + Assert.Equal(defaultValue, result.Value); + } + #endregion + + private void SetupMockResponse(string jsonContent) + { + var response = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)) + }; + + _mockAppConfigApi + .Setup(x => x.GetLatestConfigurationAsync(It.IsAny())) + .ReturnsAsync(response); + } +} diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs index cb4e6435..6e423d54 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagParserTests.cs @@ -23,7 +23,7 @@ public void ParseFeatureFlag_EnabledFlag_ReturnsValue() // Assert Assert.True(result.IsStructure); Assert.True(result.AsStructure["enabled"].AsBoolean); - Assert.Equal("testValue", result.AsStructure["additionalAttribute"].AsString); + Assert.Equal("testValue", result.AsStructure["stringAttribute"].AsString); } [Fact] diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj index 13bb68b9..38bc8414 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj @@ -5,7 +5,9 @@ + + diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json index 24b0db6d..4ca18284 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json @@ -1,7 +1,9 @@ { "test-enabled-flag": { "enabled": true, - "additionalAttribute": "testValue" + "stringAttribute": "testValue", + "doubleAttribute": 3.14, + "intAttribute": 42 }, "test-disabled-flag": { "enabled": false From 4057e5c4b4ac24bee95477bc4949e97a61469ff8 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:55:33 +0000 Subject: [PATCH 34/43] fixed test cases for appconfig provider Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigProviderTests.cs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs index 1b5e2afa..774fda48 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs @@ -160,17 +160,17 @@ public async Task ResolveStringValueAsync_WhenFlagDoesNotExist_ReturnsDefaultVal public async Task ResolveStructureValueAsync_WhenFlagExists_ReturnsCorrectValue() { // Arrange - const string flagKey = "configProfileId:test-enabled-flag"; - const string jsonValue = "{\"key\": \"value\", \"number\": 42}"; - SetupMockResponse($"{{\"{flagKey}\": {jsonValue}}}"); + const string flagKey = "configProfileId:test-enabled-flag"; + SetupMockResponse(_jsonContent); // Act var result = await _provider.ResolveStructureValueAsync(flagKey, new Value()); // Assert Assert.NotNull(result.Value.AsStructure); - Assert.Equal("value", result.Value.AsStructure["key"].AsString); - Assert.Equal(42, result.Value.AsStructure["number"].AsInteger); + Assert.True(result.Value.AsStructure["enabled"].AsBoolean); + Assert.Equal("testValue", result.Value.AsStructure["stringAttribute"].AsString); + Assert.Equal(42, result.Value.AsStructure["intAttribute"].AsInteger); } [Fact] @@ -178,10 +178,15 @@ public async Task ResolveStructureValueAsync_WhenFlagDoesNotExist_ReturnsDefault { // Arrange const string flagKey = "configProfileId:test-enabled-flag"; - var defaultValue = new Value(new Dictionary - { - ["default"] = new Value("default") - }); + var defaultValue = new Value( + new Structure( + new Dictionary + { + ["default"] = new Value("default") + } + ) + ); + SetupMockResponse("{}"); // Act @@ -198,15 +203,14 @@ public async Task ResolveStructureValueAsync_WhenFlagDoesNotExist_ReturnsDefault public async Task ResolveValue_WithAttributeKey_ReturnsAttributeValue() { // Arrange - const string flagKey = "myFlag:color"; - const string expectedValue = "blue"; - SetupMockResponse($"{{\"myFlag\": {{\"color\": \"{expectedValue}\"}}}}"); + const string flagKey = "configProfileId:test-enabled-flag:stringAttribute"; + SetupMockResponse(_jsonContent); // Act var result = await _provider.ResolveStringValueAsync(flagKey, "default"); // Assert - Assert.Equal(expectedValue, result.Value); + Assert.Equal("testValue", result.Value); } [Fact] From 3791029cedae2b3b9b12b96c8fc7c6e8d186bade Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 18 Jan 2025 03:51:04 +0000 Subject: [PATCH 35/43] fixed tests for RetrievalAPi Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigRetrievalApi.cs | 14 +- .../AppCOnfigRetrievalApiTests.cs | 280 ++++++++++++++++++ 2 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs index be047907..2404c3b4 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs @@ -51,17 +51,18 @@ public class AppConfigRetrievalApi: IRetrievalApi /// private readonly MemoryCacheEntryOptions _cacheOptions; - + /// /// Initializes a new instance of the AppConfigRetrievalApi class. /// /// The AWS AppConfig Data client used to interact with the AWS AppConfig service. + /// MemoryCache instance used for caching. If null, Default is instantiated. /// Optional duration for which items should be cached. Defaults to 5 minutes if not specified. /// Thrown when appConfigDataClient is null. - public AppConfigRetrievalApi(IAmazonAppConfigData appConfigDataClient, TimeSpan? cacheDuration = null) + public AppConfigRetrievalApi(IAmazonAppConfigData appConfigDataClient, IMemoryCache memoryCache, TimeSpan? cacheDuration = null) { _appConfigDataClient = appConfigDataClient ?? throw new ArgumentNullException(nameof(appConfigDataClient)); - _memoryCache = new MemoryCache(new MemoryCacheOptions()); + _memoryCache = memoryCache ?? new MemoryCache(new MemoryCacheOptions()); // Default cache duration of 60 minutes if not specified _cacheOptions = new MemoryCacheEntryOptions() @@ -79,9 +80,12 @@ public AppConfigRetrievalApi(IAmazonAppConfigData appConfigDataClient, TimeSpan? /// If AWS returns an empty configuration, it indicates no changes from the previous configuration, /// and the cached value will be returned if available. /// - /// Thrown when unable to connect to AWS or retrieve configuration. + /// Thrown when the provided profile is invalid. + /// Thrown when unable to connect to AWS or retrieve configuration. public async TaskGetLatestConfigurationAsync(FeatureFlagProfile profile) { + if(!profile.IsValid) throw new ArgumentException("Invalid Feature Flag configuration profile"); + var configKey = BuildConfigurationKey(profile); var sessionKey = BuildSessionKey(profile); @@ -164,7 +168,7 @@ public void Dispose() /// private async Task GetSessionToken(FeatureFlagProfile profile) { - if(!profile.IsValid) throw new ArgumentException("Invalid Feature Flag configuration profile"); + if(!profile.IsValid) throw new ArgumentException("Invalid Feature Flag configuration profile"); return await _memoryCache.GetOrCreateAsync(BuildSessionKey(profile), async entry => { diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs new file mode 100644 index 00000000..5dc0f0df --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs @@ -0,0 +1,280 @@ +using Xunit; +using Moq; +using System; +using OpenFeature.Model; +using Amazon.AppConfigData; +using Amazon.AppConfigData.Model; +using System.Text; +using OpenFeature.Contrib.Providers.AwsAppConfig; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Caching.Memory; +public class AppConfigRetrievalApiTests +{ + private readonly Mock _appConfigClientMock; + private readonly IMemoryCache _memoryCache; + private readonly AppConfigRetrievalApi _retrievalApi; + private readonly string _jsonContent; + + public AppConfigRetrievalApiTests() + { + _appConfigClientMock = new Mock(); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); + _retrievalApi = new AppConfigRetrievalApi(_appConfigClientMock.Object, _memoryCache); + _jsonContent = System.IO.File.ReadAllText("test-data.json"); + } + + [Fact] + public async Task GetConfiguration_WhenSuccessful_ReturnsConfiguration() + { + // Arrange + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = "testApp", + EnvironmentIdentifier = "testEnv", + ConfigurationProfileIdentifier = "testConfig" + }; + var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(_jsonContent)); + + var response = new GetLatestConfigurationResponse + { + Configuration = memoryStream, + NextPollConfigurationToken = "nextToken" + }; + + _appConfigClientMock + .Setup(x => x.StartConfigurationSessionAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); + + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(response); + + + + // Act + var result = await _retrievalApi.GetLatestConfigurationAsync(profile); + + // Assert + Assert.NotNull(result); + Assert.Equal(_jsonContent, await new StreamReader(result.Configuration).ReadToEndAsync()); + } + + [Fact] + public async Task GetConfiguration_WhenSuccessful_SetCorrectNextPollToken() + { + // Arrange + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = "testApp", + EnvironmentIdentifier = "testEnv", + ConfigurationProfileIdentifier = "testConfig" + }; + var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(_jsonContent)); + + var response = new GetLatestConfigurationResponse + { + Configuration = memoryStream, + NextPollConfigurationToken = "nextToken" + }; + + _appConfigClientMock + .Setup(x => x.StartConfigurationSessionAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); + + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(response); + + + + // Act + var result = await _retrievalApi.GetLatestConfigurationAsync(profile); + + // Assert + Assert.Equal("nextToken", result.NextPollConfigurationToken); + // Verify that correct sessionToken is set for Next polling. + Assert.Equal(result.NextPollConfigurationToken, _memoryCache.Get($"session_token_{profile}")); + } + + [Fact] + public async Task GetConfiguration_WhenSuccessful_CalledWithCorrectInitialToken() + { + // Arrange + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = "testApp", + EnvironmentIdentifier = "testEnv", + ConfigurationProfileIdentifier = "testConfig" + }; + var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(_jsonContent)); + + var response = new GetLatestConfigurationResponse + { + Configuration = memoryStream, + NextPollConfigurationToken = "nextToken" + }; + + _appConfigClientMock + .Setup(x => x.StartConfigurationSessionAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); + + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(response); + + + + // Act + var result = await _retrievalApi.GetLatestConfigurationAsync(profile); + + // Assert + _appConfigClientMock.Verify(x => x.GetLatestConfigurationAsync( + It.Is(r => r.ConfigurationToken == "initialToken"), + It.IsAny()), + Times.Once); + } + + [Theory] + [InlineData(null, "env", "config")] + [InlineData("app", null, "config")] + [InlineData("app", "env", null)] + public async Task GetConfiguration_WithNullParameters_ThrowsArgumentNullException( + string application, + string environment, + string configuration) + { + // Arrange + var profile = new FeatureFlagProfile + { + ApplicationIdentifier = application, + EnvironmentIdentifier = environment, + ConfigurationProfileIdentifier = configuration + }; + + // Act & Assert + await Assert.ThrowsAsync(() => + _retrievalApi.GetLatestConfigurationAsync(profile)); + } + + [Fact] + public async Task GetConfiguration_WhenServiceThrows_PropagatesException() + { + // Arrange + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = "testApp", + EnvironmentIdentifier = "testEnv", + ConfigurationProfileIdentifier = "testConfig" + }; + + _appConfigClientMock + .Setup(x => x.StartConfigurationSessionAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); + + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new AmazonAppConfigDataException("Test exception")); + + // Act & Assert + await Assert.ThrowsAsync(() => + _retrievalApi.GetLatestConfigurationAsync(profile)); + } + + [Fact] + public async Task GetConfiguration_VerifiesCorrectParametersPassedToClient() + { + // Arrange + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = "testApp", + EnvironmentIdentifier = "testEnv", + ConfigurationProfileIdentifier = "testConfig" + }; + + var response = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(), + NextPollConfigurationToken = "nextToken" + }; + + _appConfigClientMock + .Setup(x => x.StartConfigurationSessionAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); + + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(response); + + // Act + await _retrievalApi.GetLatestConfigurationAsync(profile); + + // Assert + _appConfigClientMock.Verify(x => x.GetLatestConfigurationAsync( + It.Is(r => + r.ConfigurationToken == "initialToken"), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task GetConfiguration_WhenCalledSecondTime_UsesNextPollConfigToken() + { + // Arrange + var profile = new FeatureFlagProfile{ + ApplicationIdentifier = "testApp", + EnvironmentIdentifier = "testEnv", + ConfigurationProfileIdentifier = "testConfig" + }; + var response = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(), + NextPollConfigurationToken = "nextToken" + }; + + _appConfigClientMock + .Setup(x => x.StartConfigurationSessionAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); + + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(response); + + // Act + await _retrievalApi.GetLatestConfigurationAsync(profile); + + await _retrievalApi.GetLatestConfigurationAsync(profile); + + // Assert + _appConfigClientMock.Verify(x => x.GetLatestConfigurationAsync( + It.Is(r => r.ConfigurationToken == "initialToken"), + It.IsAny()), + Times.Once); + + _appConfigClientMock.Verify(x => x.GetLatestConfigurationAsync( + It.Is(r => r.ConfigurationToken == "nextToken"), + It.IsAny()), + Times.Once); + } +} From ca58683abdfc3d29b2043042a83a8c5dbce38db2 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 18 Jan 2025 04:31:44 +0000 Subject: [PATCH 36/43] Updating for allowing RequiredMinimumPollIntervalInSeconds property to be supplied at the time of setup Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigProvider.cs | 17 +++++++++++------ .../AppConfigRetrievalApi.cs | 3 ++- .../FeatureFlagProfile.cs | 12 ++++++++++++ .../README.md | 13 +++++++++---- ...piTests.cs => AppConfigRetrievalApiTests.cs} | 2 -- 5 files changed, 34 insertions(+), 13 deletions(-) rename test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/{AppCOnfigRetrievalApiTests.cs => AppConfigRetrievalApiTests.cs} (96%) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs index 79900ae5..d5d3456b 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs @@ -19,21 +19,24 @@ public class AppConfigProvider : FeatureProvider private readonly string _applicationName; // The environment (e.g., prod, dev, staging) in AWS AppConfig - private readonly string _environmentName; + private readonly string _environmentName; + + private readonly int _minimumPollIntervalInSeconds; /// /// Returns metadata about the provider /// /// Metadata object containing provider information public override Metadata GetMetadata() => new Metadata("AWS AppConfig Provider"); - + /// /// Constructor for AwsAppConfigProvider /// /// The AWS AppConfig retrieval API /// The name of the application in AWS AppConfig - /// The environment (e.g., prod, dev, staging) in AWS AppConfig - public AppConfigProvider(IRetrievalApi retrievalApi, string applicationName, string environmentName) + /// The environment (e.g., prod, dev, staging) in AWS AppConfig + /// Client cannot call GetLatest more frequently than every specified seconds. Range 15-86400. + public AppConfigProvider(IRetrievalApi retrievalApi, string applicationName, string environmentName, int minimumPollIntervalInSeconds = 15) { // Application name, environment name and configuration profile ID is needed for connecting to AWS Appconfig. // If any of these are missing, an exception will be thrown. @@ -46,7 +49,8 @@ public AppConfigProvider(IRetrievalApi retrievalApi, string applicationName, str _appConfigRetrievalApi = retrievalApi; _applicationName = applicationName; - _environmentName = environmentName; + _environmentName = environmentName; + _minimumPollIntervalInSeconds = minimumPollIntervalInSeconds; } /// @@ -175,7 +179,7 @@ private async Task ResolveFeatureFlagValue(string flagKey, Value defaultV /// and returns it in its raw string format. The returned string is expected to be /// in JSON format that can be parsed into feature flag configurations. /// - /// Thrown when there is an error retrieving the configuration from AWS AppConfig. + /// Thrown when there is an error retrieving the configuration from AWS AppConfig. private async Task GetFeatureFlagsResponseJson(string configurationProfileId, EvaluationContext context = null) { var response = await GetFeatureFlagsStreamAsync(configurationProfileId, context); @@ -198,6 +202,7 @@ private async Task GetFeatureFlagsStreamAsync(st { ApplicationIdentifier = _applicationName, EnvironmentIdentifier = _environmentName, + RequiredMinimumPollIntervalInSeconds = _minimumPollIntervalInSeconds, ConfigurationProfileIdentifier = configurationProfileId }; diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs index 2404c3b4..10ec3c32 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs @@ -178,7 +178,8 @@ private async Task GetSessionToken(FeatureFlagProfile profile) { ApplicationIdentifier = profile.ApplicationIdentifier, EnvironmentIdentifier = profile.EnvironmentIdentifier, - ConfigurationProfileIdentifier = profile.ConfigurationProfileIdentifier + ConfigurationProfileIdentifier = profile.ConfigurationProfileIdentifier, + RequiredMinimumPollIntervalInSeconds = profile.RequiredMinimumPollIntervalInSeconds }; var sessionResponse = await _appConfigDataClient.StartConfigurationSessionAsync(request); diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs index 1a6b2ac2..43f9018a 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs @@ -19,6 +19,18 @@ public class FeatureFlagProfile /// public string EnvironmentIdentifier { get; set; } + /// + /// Gets or sets the minimum polling interval in seconds for AWS AppConfig. + /// This value determines the minimum time that must elapse between configuration refresh attempts. + /// The default value set here is 15 seconds, which aligns with AWS AppConfig's minimum supported interval. + /// Range 15 to 86400 seconds. + /// + /// + /// AWS AppConfig enforces a minimum interval of 15 seconds between configuration refresh attempts. + /// Setting a value lower than 15 seconds may result in throttling by the service. + /// + public int RequiredMinimumPollIntervalInSeconds {get; set;} = 15; + /// /// Gets or sets the AWS AppConfig configuration profile identifier. /// This identifies the specific configuration profile containing the feature flags. diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 65ed6de9..c3f6df3b 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -47,12 +47,16 @@ This package maintains the aforementioned structure by supplying values in two d **Stage 1: Setup** -During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain constant throughout the application's lifetime. If a change is necessary, a restart of the application will be required. +During this stage, the Application and Environment are provided at the initiation of the project. It is expected that these two values remain static during the application's lifetime. If a change is necessary, a restart of the application will be required. + +Additionally, at this point Required Minimum Polling Interval in seconds can also be configured by supplying integer value of seconds you would like to set. Default is 15 seconds and maximum allowed by AWS id 86400 seconds. **Stage 2: Fetching Value** In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. +## Important information about the implementation +As per AWS AppConfig documentation, the recommended way to access AWS AppConfig is by using ## Usage @@ -89,11 +93,12 @@ namespace OpenFeatureTestApp // Replace these values with your AWS AppConfig settings const string application = "YourApplication"; - const string environment = "YourEnvironment"; + const string environment = "YourEnvironment"; + const int pollingIntervalSeconds = 60; // default is 15 await Api.Instance.SetProviderAsync( - new AppConfigProvider(appConfigRetrievalApi, application, environment) - ); + new AppConfigProvider(appConfigRetrievalApi, application, environment, pollingIntervalSeconds) + ); } } } diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs similarity index 96% rename from test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs rename to test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs index 5dc0f0df..83c36717 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppCOnfigRetrievalApiTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs @@ -1,14 +1,12 @@ using Xunit; using Moq; using System; -using OpenFeature.Model; using Amazon.AppConfigData; using Amazon.AppConfigData.Model; using System.Text; using OpenFeature.Contrib.Providers.AwsAppConfig; using System.Threading; using System.Threading.Tasks; -using System.Collections.Generic; using System.IO; using Microsoft.Extensions.Caching.Memory; public class AppConfigRetrievalApiTests From 81e53f59d36da68f3938beb9d9001509e6cd23ad Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 18 Jan 2025 04:51:07 +0000 Subject: [PATCH 37/43] added important information section Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index c3f6df3b..54c02273 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -56,7 +56,11 @@ Additionally, at this point Required Minimum Polling Interval in seconds can als In this stage, to retrieve the AWS AppConfig feature flag, the key should be supplied in the format `configurationProfileId:flagKey[:attributeKey]`. If the AttributeKey is not included, all attributes will be returned as a structured object. ## Important information about the implementation -As per AWS AppConfig documentation, the recommended way to access AWS AppConfig is by using +As per AWS [AppConfig documentation](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-agent-how-to-use.html), **the recommended way for retrieving configuration data from AWS AppConfig is by using AWS AppConfig Agent, but this implemntation is not using the agents, rather uses AWS AppConfig APIs** in order to have more control around caching and parsing the returned response. + +This implementation uses in-memory IMemoryCache implementation, but any other cache can be easily swapped with if needed. + +*For detailed AWS appconfig documentation refer AWS documentation page. (link provided in References Section below).* ## Usage @@ -150,3 +154,6 @@ app.MapGet("/protected-feature", async (IFeatureClient featureClient) => .WithName("ProtectedFeature") .WithOpenApi(); ``` + +## References +1. [AWS AppConfig documentation](https://docs.aws.amazon.com/appconfig/) From 288a1e23dcbb594e4be2cf0c2357d6e7a6fc3838 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 18 Jan 2025 04:57:14 +0000 Subject: [PATCH 38/43] update readme Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 54c02273..08ad3a77 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -60,7 +60,6 @@ As per AWS [AppConfig documentation](https://docs.aws.amazon.com/appconfig/lates This implementation uses in-memory IMemoryCache implementation, but any other cache can be easily swapped with if needed. -*For detailed AWS appconfig documentation refer AWS documentation page. (link provided in References Section below).* ## Usage From c5f048ceee96ea1e6b4350fe48f1d1f7b0d7e079 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 18 Jan 2025 10:29:44 -0500 Subject: [PATCH 39/43] Update README.md for Multi-variant flag information. Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md index 08ad3a77..e5906552 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md @@ -60,6 +60,8 @@ As per AWS [AppConfig documentation](https://docs.aws.amazon.com/appconfig/lates This implementation uses in-memory IMemoryCache implementation, but any other cache can be easily swapped with if needed. +### No support for Multi-Variant flags. +This implementation currently does not support **multi-variant** AppConfig Feature flags. Or rather there is no way to pass on calling context to the request to AWS AppConfig. I am looking at documentation to figure out how this is done, but haven't got much far on that. Will be looking to add that soon. ## Usage From f2a99078a4f253bafdb2bf8aadab2f7c419f1ab0 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:38:39 +0000 Subject: [PATCH 40/43] fix: AppConfig through exception if calling too frequently, use whats in cache Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigRetrievalApi.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs index 10ec3c32..2e285d15 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs @@ -95,15 +95,27 @@ public async TaskGetLatestConfigurationAsync(Fea ConfigurationToken = await GetSessionToken(profile) }; - var response = await _appConfigDataClient.GetLatestConfigurationAsync(configurationRequest); + GetLatestConfigurationResponse response; - // If not NextPollConfigurationToken, something wrong with AWS connection. - if(string.IsNullOrWhiteSpace(response.NextPollConfigurationToken)) throw new Exception("Unable to connect to AWS"); + try + { + response = await _appConfigDataClient.GetLatestConfigurationAsync(configurationRequest); + } + catch + { + // On exception, could be because of connection issue or + // too frequent call per defined by polling duration, get what's in cache + response = null; + } - // First, update the session token to the newly returned token - _memoryCache.Set(sessionKey, response.NextPollConfigurationToken); + // Update Next Poll configuration token only when one is available. + if(response != null) + { + // First, update the session token to the newly returned token + _memoryCache.Set(sessionKey, response.NextPollConfigurationToken); + } - if((response.Configuration == null || response.Configuration.Length == 0) + if((response?.Configuration == null || response.Configuration.Length == 0) && _memoryCache.TryGetValue(configKey, out GetLatestConfigurationResponse configValue)) { // AppConfig returns empty Configuration if value hasn't changed from last retrieval, hence use what's in cache. From 79e4f0b4db139cf2c0f3beed1b34de69835d5a57 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Sat, 18 Jan 2025 11:47:51 -0500 Subject: [PATCH 41/43] Update FeatureFlagProfile.cs - updated comments Missed updating comments over the course of changes. Fixing that. Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../FeatureFlagProfile.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs index 43f9018a..c66277f7 100644 --- a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs +++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs @@ -18,6 +18,12 @@ public class FeatureFlagProfile /// This represents the deployment environment (e.g., development, production) in AWS AppConfig. /// public string EnvironmentIdentifier { get; set; } + + /// + /// Gets or sets the AWS AppConfig configuration profile identifier. + /// This identifies the specific configuration profile containing the feature flags. + /// + public string ConfigurationProfileIdentifier { get; set; } /// /// Gets or sets the minimum polling interval in seconds for AWS AppConfig. @@ -31,12 +37,6 @@ public class FeatureFlagProfile /// public int RequiredMinimumPollIntervalInSeconds {get; set;} = 15; - /// - /// Gets or sets the AWS AppConfig configuration profile identifier. - /// This identifies the specific configuration profile containing the feature flags. - /// - public string ConfigurationProfileIdentifier { get; set; } - /// /// Gets a value indicating whether the profile is valid. /// A profile is considered valid when all identifiers (Application, Environment, and Configuration Profile) @@ -48,7 +48,7 @@ public class FeatureFlagProfile /// /// Returns a string representation of the feature flag profile. - /// The format is "ApplicationIdentifier+EnvironmentIdentifier+ConfigurationProfileIdentifier". + /// The format is "ApplicationIdentifier_EnvironmentIdentifier_ConfigurationProfileIdentifier". /// /// A string containing all three identifiers concatenated with '+' characters. public override string ToString() From a14bc85ae83b728e4e06592fc3aa53e43c4b1975 Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:51:23 +0000 Subject: [PATCH 42/43] fixing broken test after exception catching when calling AWS Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigRetrievalApiTests.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs index 83c36717..d8163b56 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs @@ -167,7 +167,7 @@ await Assert.ThrowsAsync(() => } [Fact] - public async Task GetConfiguration_WhenServiceThrows_PropagatesException() + public async Task GetConfiguration_WhenServiceThrows_DoNotPropagatesException() { // Arrange var profile = new FeatureFlagProfile{ @@ -181,16 +181,18 @@ public async Task GetConfiguration_WhenServiceThrows_PropagatesException() It.IsAny(), It.IsAny())) .ReturnsAsync(new StartConfigurationSessionResponse{InitialConfigurationToken="initialToken"}); - - _appConfigClientMock - .Setup(x => x.GetLatestConfigurationAsync( - It.IsAny(), - It.IsAny())) - .ThrowsAsync(new AmazonAppConfigDataException("Test exception")); - - // Act & Assert - await Assert.ThrowsAsync(() => - _retrievalApi.GetLatestConfigurationAsync(profile)); + try + { + _appConfigClientMock + .Setup(x => x.GetLatestConfigurationAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new AmazonAppConfigDataException("Test exception")); + } + catch (Exception e) + { + Assert.Null(e); // No exception expected + } } [Fact] From c31db95e794c81ce87ce23501256e6eeddeda4ba Mon Sep 17 00:00:00 2001 From: "Ash.Wani" <149169001+wani-guanxi@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:52:58 +0000 Subject: [PATCH 43/43] modifying comment to signoff Signed-off-by: Ash.Wani <149169001+wani-guanxi@users.noreply.github.com> --- .../AppConfigRetrievalApiTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs index d8163b56..6a8c1a7d 100644 --- a/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs +++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs @@ -191,7 +191,7 @@ public async Task GetConfiguration_WhenServiceThrows_DoNotPropagatesException() } catch (Exception e) { - Assert.Null(e); // No exception expected + Assert.Null(e); // No exception expected also signing off } }