diff --git a/README.md b/README.md index 08e334f..3ec138b 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,51 @@ namespace CustomParameterProcessorExample } ``` +### Simple AppConfig FeatureFlag Sample +```csharp +// +// AppConfigFeatureFlag.cs +// +public class AppConfigFeatureFlag +{ + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } +} + +// +// Program.cs +// + +// Map feature flag configuration +builder.Services.Configure>(builder.Configuration.GetSection("FeatureFlags")); + +// Add AppConfig configuration source +var awsAppConfigClient = new AmazonAppConfigDataClient(); +builder.Configuration + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddAppConfig(new AppConfigConfigurationSource() + { + ApplicationId = "application", + EnvironmentId = "Default", + ConfigProfileId = "my-appconfig-feature-profile", + AwsOptions = new AWSOptions(), + ReloadAfter = new TimeSpan(0, 0, 30), + WrapperNodeName = "FeatureFlags" // Nest AppConfig FeatureFlag data under this node + }); + +// +// TestApi.cs +// +public class TestApi : ITestApi +{ + private readonly IOptionsMonitor> _FeatureFlags; + public TestApi(IOptionsMonitor> featureFlags) + { + _FeatureFlags = featureFlags; + } +} +``` + For more complete examples, take a look at sample projects available in [samples directory](https://github.com/aws/aws-dotnet-extensions-configuration/tree/master/samples). # Configuring Systems Manager Client diff --git a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigConfigurationSource.cs b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigConfigurationSource.cs index 9e92d0a..4dea75b 100644 --- a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigConfigurationSource.cs +++ b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigConfigurationSource.cs @@ -51,6 +51,11 @@ public class AppConfigConfigurationSource : ISystemsManagerConfigurationSource /// public TimeSpan? ReloadAfter { get; set; } + + /// + /// Name of the node to wrap the configuration data in. + /// + public string WrapperNodeName { get; set; } /// /// Indicates to use configured lambda extension HTTP client to retrieve AppConfig data. diff --git a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigExtensions.cs b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigExtensions.cs index 1e890d8..a78e9ce 100644 --- a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigExtensions.cs +++ b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigExtensions.cs @@ -238,7 +238,8 @@ private static AppConfigConfigurationSource ConfigureSource( string configProfileId, AWSOptions awsOptions = null, bool optional = false, - TimeSpan? reloadAfter = null + TimeSpan? reloadAfter = null, + string wrapperNodeName = null ) { return new AppConfigConfigurationSource @@ -248,7 +249,8 @@ private static AppConfigConfigurationSource ConfigureSource( ConfigProfileId = configProfileId, AwsOptions = awsOptions, Optional = optional, - ReloadAfter = reloadAfter + ReloadAfter = reloadAfter, + WrapperNodeName = wrapperNodeName }; } } diff --git a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs index 094fde8..371b187 100644 --- a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs +++ b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs @@ -93,6 +93,19 @@ public async Task> GetDataAsync() ?? new Dictionary(); } } + + private async Task AddWrapperNodeAsync(Stream configStream, Stream wrappedStream) + { + string wrappedConfig; + using (var reader = new StreamReader(configStream)) + { + wrappedConfig = $"{{\"{Source.WrapperNodeName}\":{await reader.ReadToEndAsync().ConfigureAwait(false)}}}"; + } + + var wrappedConfigBytes = System.Text.Encoding.UTF8.GetBytes(wrappedConfig); + await wrappedStream.WriteAsync(wrappedConfigBytes, 0, wrappedConfigBytes.Length).ConfigureAwait(false); + wrappedStream.Position = 0; + } private async Task> GetDataFromLambdaExtensionAsync() { @@ -134,7 +147,18 @@ private async Task> GetDataFromServiceAsync() // so only attempt to parse the AppConfig response when it is not empty if (response.ContentLength > 0) { - LastConfig = ParseConfig(response.ContentType, response.Configuration); + if (string.IsNullOrWhiteSpace(Source.WrapperNodeName)) + { + LastConfig = ParseConfig(response.ContentType, response.Configuration); + } + else + { + using (var wrappedConfiguration = new MemoryStream()) + { + await AddWrapperNodeAsync(response.Configuration, wrappedConfiguration).ConfigureAwait(false); + LastConfig = ParseConfig(response.ContentType, wrappedConfiguration); + } + } } } finally diff --git a/test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs b/test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs new file mode 100644 index 0000000..e5973b5 --- /dev/null +++ b/test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Amazon.Extensions.Configuration.SystemsManager.AppConfig; +using Amazon.Extensions.NETCore.Setup; +using Xunit; + +namespace Amazon.Extensions.Configuration.SystemsManager.Tests +{ + public class AppConfigProcessorTests + { + [Fact] + public async Task AddWrapperNode_WithWrapperNodeName_WrapsConfigurationCorrectly() + { + // Arrange + var source = new AppConfigConfigurationSource + { + ApplicationId = "appId", + EnvironmentId = "envId", + ConfigProfileId = "profileId", + WrapperNodeName = "FeatureFlags", + AwsOptions = new AWSOptions() + }; + + var processor = new AppConfigProcessor(source); + var inputJson = "{\"test-flag-1\":{\"enabled\":false},\"test-flag-2\":{\"enabled\":true}}"; + using var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(inputJson)); + using var outputStream = new MemoryStream(); + + // Act + await InvokeAddWrapperNode(processor, inputStream, outputStream); + + // Assert + string resultJson; + using (var reader = new StreamReader(outputStream)) + { + resultJson = reader.ReadToEnd(); + } + + var expectedJson = "{\"FeatureFlags\":{\"test-flag-1\":{\"enabled\":false},\"test-flag-2\":{\"enabled\":true}}}"; + Assert.Equal(expectedJson, resultJson); + } + + // Helper method to invoke the private AddWrapperNode method using reflection + private async Task InvokeAddWrapperNode(AppConfigProcessor processor, Stream configuration, Stream wrappedConfig) + { + var method = typeof(AppConfigProcessor).GetMethod("AddWrapperNodeAsync", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + await (Task)method.Invoke(processor, new object[] { configuration, wrappedConfig }); + } + } +} \ No newline at end of file