diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln
index 570043860..1545da07e 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/AppConfigKey.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs
new file mode 100644
index 000000000..097508c01
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigKey.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Text.RegularExpressions;
+
+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 App config's Configuration Profile ID
+ ///
+ public string ConfigurationProfileId {get; }
+
+ ///
+ /// 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.IsNullOrWhiteSpace(AttributeKey);
+
+ ///
+ /// Initializes a new instance of the class that represents a structured key
+ /// for AWS AppConfig feature flags.
+ ///
+ ///
+ /// 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))
+ {
+ 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 )
+ {
+ throw new ArgumentException("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format");
+ }
+
+ ConfigurationProfileId = parts[0];
+ FlagKey = parts[1];
+ // At this point, AWS AppConfig allows only value types for attributes.
+ // Hence ignoring anything afterwords.
+ if (parts.Length > 2)
+ {
+ 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 ArgumentNullException("Configuration Profile ID cannot be null or empty");
+ }
+
+ if (string.IsNullOrWhiteSpace(flagKey))
+ {
+ throw new ArgumentNullException("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 override string ToString()
+ {
+ return $"{ConfigurationProfileId}{Separator}{FlagKey}{(HasAttribute ? Separator + AttributeKey : "")}";
+ }
+ }
+}
diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs
new file mode 100644
index 000000000..d5d3456bb
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigProvider.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenFeature.Model;
+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 AppConfigProvider : FeatureProvider
+ {
+ // AWS AppConfig client for interacting with the service
+ 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;
+
+ 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
+ /// 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.
+
+ if (string.IsNullOrEmpty(applicationName))
+ throw new ArgumentNullException(nameof(applicationName));
+
+ if (string.IsNullOrEmpty(environmentName))
+ throw new ArgumentNullException(nameof(environmentName));
+
+ _appConfigRetrievalApi = retrievalApi;
+ _applicationName = applicationName;
+ _environmentName = environmentName;
+ _minimumPollIntervalInSeconds = minimumPollIntervalInSeconds;
+ }
+
+ ///
+ /// Resolves a boolean feature flag value
+ ///
+ /// 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 attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue));
+ return new ResolutionDetails(flagKey, attributeValue.AsBoolean ?? defaultValue);
+ }
+
+ ///
+ /// Resolves a double feature flag value
+ ///
+ /// 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 attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue));
+ return new ResolutionDetails(flagKey, attributeValue.AsDouble ?? defaultValue);
+ }
+
+ ///
+ /// Resolves an integer feature flag value
+ ///
+ /// 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 attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue));
+ return new ResolutionDetails(flagKey, attributeValue.AsInteger ?? defaultValue);
+ }
+
+ ///
+ /// Resolves a string feature flag value
+ ///
+ /// 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 attributeValue = await ResolveFeatureFlagValue(flagKey, new Value(defaultValue));
+ return new ResolutionDetails(flagKey, attributeValue.AsString ?? defaultValue);
+ }
+
+ ///
+ /// 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 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 appConfigKey = new AppConfigKey(flagKey);
+
+ var responseString = await GetFeatureFlagsResponseJson(appConfigKey.ConfigurationProfileId);
+
+ var flagValues = FeatureFlagParser.ParseFeatureFlag(appConfigKey.FlagKey, defaultValue, responseString);
+
+ if (!appConfigKey.HasAttribute) return flagValues;
+
+ var structuredValues = flagValues.AsStructure;
+
+ if(structuredValues == null) return defaultValue;
+
+ return structuredValues.TryGetValue(appConfigKey.AttributeKey, out var returnValue) ? returnValue : defaultValue;
+ }
+
+
+ ///
+ /// 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(string configurationProfileId, EvaluationContext context = null)
+ {
+ var response = await GetFeatureFlagsStreamAsync(configurationProfileId, context);
+ return System.Text.Encoding.UTF8.GetString(response.Configuration.ToArray());
+ }
+
+ ///
+ /// 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
+ ///
+ ///
+ private async Task GetFeatureFlagsStreamAsync(string configurationProfileId, EvaluationContext context = null)
+ {
+ var profile = new FeatureFlagProfile
+ {
+ ApplicationIdentifier = _applicationName,
+ EnvironmentIdentifier = _environmentName,
+ RequiredMinimumPollIntervalInSeconds = _minimumPollIntervalInSeconds,
+ ConfigurationProfileIdentifier = configurationProfileId
+ };
+
+ return await _appConfigRetrievalApi.GetLatestConfigurationAsync(profile);
+ }
+ }
+}
diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs
new file mode 100644
index 000000000..2e285d151
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/AppConfigRetrievalApi.cs
@@ -0,0 +1,227 @@
+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: 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;
+
+ ///
+ /// AWS AppConfig Data client used to interact with the AWS AppConfig service.
+ ///
+ private readonly IAmazonAppConfigData _appConfigDataClient;
+
+ ///
+ /// 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.
+ /// 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, IMemoryCache memoryCache, TimeSpan? cacheDuration = null)
+ {
+ _appConfigDataClient = appConfigDataClient ?? throw new ArgumentNullException(nameof(appConfigDataClient));
+ _memoryCache = 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 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.
+ ///
+ /// 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 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);
+
+ // Build GetLatestConfiguration Request
+ var configurationRequest = new GetLatestConfigurationRequest
+ {
+ ConfigurationToken = await GetSessionToken(profile)
+ };
+
+ GetLatestConfigurationResponse response;
+
+ 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;
+ }
+
+ // 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)
+ && _memoryCache.TryGetValue(configKey, out GetLatestConfigurationResponse configValue))
+ {
+ // AppConfig returns empty 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;
+ }
+ }
+
+ ///
+ /// 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));
+ }
+
+ ///
+ /// 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)
+ {
+ 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");
+
+ return await _memoryCache.GetOrCreateAsync(BuildSessionKey(profile), async entry =>
+ {
+ entry.SetOptions(_cacheOptions);
+
+ var request = new StartConfigurationSessionRequest
+ {
+ ApplicationIdentifier = profile.ApplicationIdentifier,
+ EnvironmentIdentifier = profile.EnvironmentIdentifier,
+ ConfigurationProfileIdentifier = profile.ConfigurationProfileIdentifier,
+ RequiredMinimumPollIntervalInSeconds = profile.RequiredMinimumPollIntervalInSeconds
+ };
+
+ var sessionResponse = await _appConfigDataClient.StartConfigurationSessionAsync(request);
+ // We only need Initial Configuration Token from starting the session.
+ return sessionResponse.InitialConfigurationToken;
+ });
+ }
+
+ ///
+ /// 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}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagParser.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagParser.cs
new file mode 100644
index 000000000..2045c7a64
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagParser.cs
@@ -0,0 +1,117 @@
+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 FeatureFlagParser
+ {
+ ///
+ /// 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()));
+ }
+ else
+ {
+ 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);
+ }
+ }
+}
+
+
diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs
new file mode 100644
index 000000000..c66277f7a
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/FeatureFlagProfile.cs
@@ -0,0 +1,59 @@
+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 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 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/IRetrievalApi.cs b/src/OpenFeature.Contrib.Providers.AwsAppConfig/IRetrievalApi.cs
new file mode 100644
index 000000000..f0621361b
--- /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);
+ }
+}
+
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 000000000..d3b11b879
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/OpenFeature.Contrib.Providers.AwsAppConfig.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0;net7.0;net6.0;
+ 7.3
+ OpenFeature.Contrib.Providers.AwsAppConfig
+ 0.0.1
+ $(VersionNumber)
+ $(VersionNumber)
+ $(VersionNumber)
+ AWS AppConfig provider for .NET
+ wani-guanxi
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).Test
+
+
+
+
+
+
+
diff --git a/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md
new file mode 100644
index 000000000..e59065522
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.AwsAppConfig/README.md
@@ -0,0 +1,160 @@
+# 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 AWS AppConfig to work with
+- 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
+```
+
+## 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.
+
+```
+Application
+└── Environment
+ └── ConfigurationProfileId
+ └── FlagKey
+ └── AttributeKey
+```
+
+### 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.
+
+- **FlagKey**: Toggles that control the availability of specific features within the application.
+
+- **AttributeKey**: Additional properties associated with each feature flag (e.g., enabled status, description).
+
+### Representation
+
+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 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](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.
+
+### 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
+
+### 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";
+ const int pollingIntervalSeconds = 60; // default is 15
+
+ await Api.Instance.SetProviderAsync(
+ new AppConfigProvider(appConfigRetrievalApi, application, environment, pollingIntervalSeconds)
+ );
+ }
+ }
+}
+```
+
+### Example Usage
+
+#### Example endpoints using feature flags
+
+```csharp
+// Example endpoints using feature flags
+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.ToString(), false);
+ return Results.Ok(new { FeatureEnabled = isEnabled });
+})
+.WithName("GetFeatureStatus")
+.WithOpenApi();
+
+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.ToString(), "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 key = new AppConfigKey(configurationProfileId, "protected-feature", "enabled");
+ var isFeatureEnabled = await featureClient.GetBooleanValue(key.ToString(), false);
+
+ if (!isFeatureEnabled)
+ {
+ return Results.NotFound(new { Message = "Feature not available" });
+ }
+
+ return Results.Ok(new { Message = "Feature is enabled!" });
+})
+.WithName("ProtectedFeature")
+.WithOpenApi();
+```
+
+## References
+1. [AWS AppConfig documentation](https://docs.aws.amazon.com/appconfig/)
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 000000000..be4e82242
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigKeyTests.cs
@@ -0,0 +1,265 @@
+using Xunit;
+using OpenFeature.Contrib.Providers.AwsAppConfig;
+using System;
+
+public class AppConfigKeyTests
+{
+ [Fact]
+ public void Constructor_3input_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(null, "env", "config")]
+ [InlineData("app", null, "config")]
+ public void Constructor_3input_WithInvalidParameters_ShouldThrowArgumentException(
+ string confiProfileId, string flagKey, string attributeKey)
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ new AppConfigKey(confiProfileId, flagKey, attributeKey));
+ }
+
+ [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_3input_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);
+ }
+
+ [Fact]
+ public void Constructor_3input_WithWhitespaceValues_ShouldThrowArgumentException()
+ {
+ // Arrange
+ var application = " ";
+ var environment = "env";
+ var configuration = "config";
+
+ // Act & Assert
+ Assert.Throws(() =>
+ new AppConfigKey(application, environment, configuration));
+ }
+
+ [Fact]
+ 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 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);
+ }
+
+ [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(key));
+ Assert.Equal("Invalid key format. Flag key is expected in configurationProfileId:flagKey[:attributeKey] format", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("profile123::attr789")]
+ [InlineData(":flag456:attr789")]
+ [InlineData("::attr789")]
+ 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_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);
+ }
+
+ [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);
+ }
+}
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 000000000..774fda485
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigProviderTests.cs
@@ -0,0 +1,243 @@
+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";
+ SetupMockResponse(_jsonContent);
+
+ // Act
+ var result = await _provider.ResolveStructureValueAsync(flagKey, new Value());
+
+ // Assert
+ Assert.NotNull(result.Value.AsStructure);
+ Assert.True(result.Value.AsStructure["enabled"].AsBoolean);
+ Assert.Equal("testValue", result.Value.AsStructure["stringAttribute"].AsString);
+ Assert.Equal(42, result.Value.AsStructure["intAttribute"].AsInteger);
+ }
+
+ [Fact]
+ public async Task ResolveStructureValueAsync_WhenFlagDoesNotExist_ReturnsDefaultValue()
+ {
+ // Arrange
+ const string flagKey = "configProfileId:test-enabled-flag";
+ var defaultValue = new Value(
+ new Structure(
+ 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 = "configProfileId:test-enabled-flag:stringAttribute";
+ SetupMockResponse(_jsonContent);
+
+ // Act
+ var result = await _provider.ResolveStringValueAsync(flagKey, "default");
+
+ // Assert
+ Assert.Equal("testValue", 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/AppConfigRetrievalApiTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs
new file mode 100644
index 000000000..6a8c1a7da
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/AppConfigRetrievalApiTests.cs
@@ -0,0 +1,280 @@
+using Xunit;
+using Moq;
+using System;
+using Amazon.AppConfigData;
+using Amazon.AppConfigData.Model;
+using System.Text;
+using OpenFeature.Contrib.Providers.AwsAppConfig;
+using System.Threading;
+using System.Threading.Tasks;
+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_DoNotPropagatesException()
+ {
+ // 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"});
+ try
+ {
+ _appConfigClientMock
+ .Setup(x => x.GetLatestConfigurationAsync(
+ It.IsAny(),
+ It.IsAny()))
+ .ThrowsAsync(new AmazonAppConfigDataException("Test exception"));
+ }
+ catch (Exception e)
+ {
+ Assert.Null(e); // No exception expected also signing off
+ }
+ }
+
+ [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);
+ }
+}
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 000000000..6e423d543
--- /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["stringAttribute"].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/FeatureFlagProfileTests.cs b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/FeatureFlagProfileTests.cs
new file mode 100644
index 000000000..af871aebc
--- /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
new file mode 100644
index 000000000..38bc84146
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/OpenFeature.Contrib.Providers.AwsAppConfig.Test.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 000000000..4ca182841
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.AwsAppConfig.Test/test-data.json
@@ -0,0 +1,11 @@
+{
+ "test-enabled-flag": {
+ "enabled": true,
+ "stringAttribute": "testValue",
+ "doubleAttribute": 3.14,
+ "intAttribute": 42
+ },
+ "test-disabled-flag": {
+ "enabled": false
+ }
+}