From dfef2e657a7b784e90e36ca57eae48a24a4a0638 Mon Sep 17 00:00:00 2001
From: m-nash <64171366+m-nash@users.noreply.github.com>
Date: Wed, 4 Mar 2026 15:10:09 -0800
Subject: [PATCH] Add AddAppConfigurations extension for config-section-based
client construction
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../ConsoleAppWithFailOver.csproj | 2 +-
.../AzureAppConfigurationExtensions.cs | 77 +++++++++
.../ExperimentalAttribute.cs | 48 ++++++
...Configuration.AzureAppConfiguration.csproj | 5 +-
.../docs/ExperimentalFeatures.md | 54 +++++++
.../Tests.AzureAppConfiguration.csproj | 2 +-
.../Unit/ConfigurationSettingsTests.cs | 151 ++++++++++++++++++
7 files changed, 335 insertions(+), 4 deletions(-)
create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ExperimentalAttribute.cs
create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/docs/ExperimentalFeatures.md
create mode 100644 tests/Tests.AzureAppConfiguration/Unit/ConfigurationSettingsTests.cs
diff --git a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
index 09732dbf..fcdc6572 100644
--- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
+++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs
index a5657c2b..2540ad46 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs
@@ -2,11 +2,14 @@
// Licensed under the MIT license.
//
using Azure.Core;
+using Azure.Data.AppConfiguration;
+using Azure.Identity;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.Configuration
{
@@ -102,6 +105,80 @@ public static IConfigurationBuilder AddAzureAppConfiguration(
return configurationBuilder;
}
+ ///
+ /// Adds key-value data from an Azure App Configuration store to a configuration builder.
+ /// The is created from the specified configuration section using
+ /// .
+ ///
+ /// The configuration builder to add key-values to.
+ /// The name of the configuration section that contains the .
+ /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
+ ///
+ /// The provided configuration builder.
+ ///
+ /// For more information on configuring Azure clients from configuration, see
+ /// Configuration and Dependency Injection.
+ ///
+ [Experimental("SCME0002")]
+ public static IConfigurationBuilder AddAppConfigurations(
+ this IConfigurationBuilder configurationBuilder,
+ string sectionName,
+ bool optional = false)
+ {
+ return AddAppConfigurations(configurationBuilder, sectionName, null, optional);
+ }
+
+ ///
+ /// Adds key-value data from an Azure App Configuration store to a configuration builder.
+ /// The is created from the specified configuration section using
+ /// . The callback can be used
+ /// to further configure the provider (e.g., selecting keys, configuring refresh, using feature flags).
+ ///
+ /// The configuration builder to add key-values to.
+ /// The name of the configuration section that contains the .
+ /// An optional callback used to configure Azure App Configuration options.
+ /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
+ ///
+ /// The provided configuration builder.
+ ///
+ /// For more information on configuring Azure clients from configuration, see
+ /// Configuration and Dependency Injection.
+ ///
+ [Experimental("SCME0002")]
+ public static IConfigurationBuilder AddAppConfigurations(
+ this IConfigurationBuilder configurationBuilder,
+ string sectionName,
+ Action action,
+ bool optional = false)
+ {
+ if (configurationBuilder == null)
+ {
+ throw new ArgumentNullException(nameof(configurationBuilder));
+ }
+
+ if (string.IsNullOrEmpty(sectionName))
+ {
+ throw new ArgumentException("Value cannot be null or empty.", nameof(sectionName));
+ }
+
+ if (_isProviderDisabled)
+ {
+ return configurationBuilder;
+ }
+
+ IConfiguration configuration = configurationBuilder.Build();
+ ConfigurationClientSettings settings = configuration.GetAzureClientSettings(sectionName);
+ TokenCredential credential = (TokenCredential)settings.CredentialProvider;
+
+ return configurationBuilder.AddAzureAppConfiguration(options =>
+ {
+ options.Connect(settings.Endpoint, credential);
+ action?.Invoke(options);
+ }, optional);
+ }
+
///
/// Adds Azure App Configuration services to the specified .
///
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ExperimentalAttribute.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ExperimentalAttribute.cs
new file mode 100644
index 00000000..ec7aa836
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ExperimentalAttribute.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+
+#if !NET8_0_OR_GREATER
+
+#nullable enable
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Indicates that an API is experimental and it may change in the future.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly |
+ AttributeTargets.Module |
+ AttributeTargets.Class |
+ AttributeTargets.Struct |
+ AttributeTargets.Enum |
+ AttributeTargets.Constructor |
+ AttributeTargets.Method |
+ AttributeTargets.Property |
+ AttributeTargets.Field |
+ AttributeTargets.Event |
+ AttributeTargets.Interface |
+ AttributeTargets.Delegate, Inherited = false)]
+ internal sealed class ExperimentalAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ID that the compiler will use when reporting a use of the API the attribute applies to.
+ public ExperimentalAttribute(string diagnosticId)
+ {
+ DiagnosticId = diagnosticId;
+ }
+
+ ///
+ /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to.
+ ///
+ public string DiagnosticId { get; }
+
+ ///
+ /// Gets or sets the URL for corresponding documentation.
+ ///
+ public string? UrlFormat { get; set; }
+ }
+}
+#endif
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
index 8c8241a7..e7f1b662 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -15,14 +15,15 @@
-
+
+
-
+
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/docs/ExperimentalFeatures.md b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/docs/ExperimentalFeatures.md
new file mode 100644
index 00000000..54c9a916
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/docs/ExperimentalFeatures.md
@@ -0,0 +1,54 @@
+# Experimental Feature Diagnostics
+
+This document lists the experimental feature diagnostic IDs used in the Azure App Configuration .NET Provider to mark APIs that are under development and subject to change.
+
+## SCME0002 - Configuration-Based Client Construction
+
+### Description
+
+The `AddAppConfigurations` extension methods on `IConfigurationBuilder` are experimental APIs that allow constructing an Azure App Configuration client directly from an `IConfiguration` section. These methods use `ConfigurationClientSettings` from `Azure.Data.AppConfiguration` and `GetAzureClientSettings` from `Azure.Identity` to read the endpoint and credential from configuration, eliminating the need for manual client construction.
+
+These APIs depend on experimental features from the Azure SDK (`Azure.Data.AppConfiguration` and `Azure.Core`) that are also marked with `SCME0002`. They are subject to change or removal in future updates as the underlying SDK APIs stabilize.
+
+### Affected APIs
+
+- `AzureAppConfigurationExtensions.AddAppConfigurations(IConfigurationBuilder, string, bool)` — Adds App Configuration using a named configuration section.
+- `AzureAppConfigurationExtensions.AddAppConfigurations(IConfigurationBuilder, string, Action, bool)` — Adds App Configuration using a named configuration section with additional options configuration.
+
+### Example Usage
+
+```csharp
+// appsettings.json
+// {
+// "AppConfiguration": {
+// "Endpoint": "https://.azconfig.io",
+// "Credential": {
+// "CredentialSource": "AzureCli"
+// }
+// }
+// }
+
+var builder = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .AddAppConfigurations("AppConfiguration");
+```
+
+For more information on the configuration schema, see the [Azure.Core Configuration and Dependency Injection](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src/docs/ConfigurationAndDependencyInjection.md) documentation.
+
+For the upstream `SCME0002` diagnostic defined in Azure.Core, see the [Azure.Core Experimental Features](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src/docs/ExperimentalFeatures.md) documentation.
+
+### Suppression
+
+If you want to use these experimental APIs and accept the risk that they may change, you can suppress the warning:
+
+```csharp
+#pragma warning disable SCME0002 // Type is for evaluation purposes only and is subject to change or removal in future updates.
+```
+
+Or in your project file:
+
+```xml
+
+ $(NoWarn);SCME0002
+
+```
diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index 1e5aaae1..58586791 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/tests/Tests.AzureAppConfiguration/Unit/ConfigurationSettingsTests.cs b/tests/Tests.AzureAppConfiguration/Unit/ConfigurationSettingsTests.cs
new file mode 100644
index 00000000..df4136b4
--- /dev/null
+++ b/tests/Tests.AzureAppConfiguration/Unit/ConfigurationSettingsTests.cs
@@ -0,0 +1,151 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+#pragma warning disable SCME0002 // Experimental API
+
+namespace Tests.AzureAppConfiguration
+{
+ public class ConfigurationSettingsTests
+ {
+ private const string SectionName = "AppConfiguration";
+ private const string TestEndpoint = "https://azure.azconfig.io";
+
+ private static Dictionary CreateConfigSection()
+ {
+ return new Dictionary
+ {
+ { $"{SectionName}:Endpoint", TestEndpoint },
+ { $"{SectionName}:Credential:CredentialSource", "AzureCli" }
+ };
+ }
+
+ [Fact]
+ public void AddAppConfigurationsThrowsOnNullBuilder()
+ {
+ Assert.Throws(() =>
+ AzureAppConfigurationExtensions.AddAppConfigurations(null, SectionName));
+ }
+
+ [Fact]
+ public void AddAppConfigurationsThrowsOnNullSectionName()
+ {
+ var builder = new ConfigurationBuilder();
+ Assert.Throws(() =>
+ builder.AddAppConfigurations(null));
+ }
+
+ [Fact]
+ public void AddAppConfigurationsThrowsOnEmptySectionName()
+ {
+ var builder = new ConfigurationBuilder();
+ Assert.Throws(() =>
+ builder.AddAppConfigurations(string.Empty));
+ }
+
+ [Fact]
+ public void AddAppConfigurationsWithActionThrowsOnNullBuilder()
+ {
+ Assert.Throws(() =>
+ AzureAppConfigurationExtensions.AddAppConfigurations(null, SectionName, options => { }));
+ }
+
+ [Fact]
+ public void AddAppConfigurationsWithActionThrowsOnNullSectionName()
+ {
+ var builder = new ConfigurationBuilder();
+ Assert.Throws(() =>
+ builder.AddAppConfigurations(null, options => { }));
+ }
+
+ [Fact]
+ public void AddAppConfigurationsWithActionThrowsOnEmptySectionName()
+ {
+ var builder = new ConfigurationBuilder();
+ Assert.Throws(() =>
+ builder.AddAppConfigurations(string.Empty, options => { }));
+ }
+
+ [Fact]
+ public void AddAppConfigurationsAddsSourceToBuilder()
+ {
+ // Arrange
+ int initialSourceCount = 1; // the in-memory source
+
+ IConfigurationBuilder builder = new ConfigurationBuilder()
+ .AddInMemoryCollection(CreateConfigSection());
+
+ // Act
+ builder.AddAppConfigurations(SectionName);
+
+ // Assert - A new source should have been added beyond the initial in-memory source
+ Assert.Equal(initialSourceCount + 1, builder.Sources.Count);
+ }
+
+ [Fact]
+ public void AddAppConfigurationsWithActionAddsSourceToBuilder()
+ {
+ // Arrange
+ int initialSourceCount = 1;
+
+ IConfigurationBuilder builder = new ConfigurationBuilder()
+ .AddInMemoryCollection(CreateConfigSection());
+
+ // Act
+ builder.AddAppConfigurations(SectionName, options => { });
+
+ // Assert
+ Assert.Equal(initialSourceCount + 1, builder.Sources.Count);
+ }
+
+ [Fact]
+ public void AddAppConfigurationsOptionalBuildSucceeds()
+ {
+ // Arrange
+ IConfigurationBuilder builder = new ConfigurationBuilder()
+ .AddInMemoryCollection(CreateConfigSection())
+ .AddAppConfigurations(SectionName, options =>
+ {
+ options.ConfigureStartupOptions(startupOptions =>
+ {
+ startupOptions.Timeout = TimeSpan.FromMilliseconds(1);
+ });
+ }, optional: true);
+
+ // Act - Build should succeed because optional=true suppresses load errors
+ IConfigurationRoot config = builder.Build();
+
+ // Assert
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void AddAppConfigurationsWithActionInvokesCallback()
+ {
+ // Arrange
+ bool actionInvoked = false;
+
+ IConfigurationBuilder builder = new ConfigurationBuilder()
+ .AddInMemoryCollection(CreateConfigSection())
+ .AddAppConfigurations(SectionName, options =>
+ {
+ actionInvoked = true;
+ options.ConfigureStartupOptions(startupOptions =>
+ {
+ startupOptions.Timeout = TimeSpan.FromMilliseconds(1);
+ });
+ }, optional: true);
+
+ // Act
+ builder.Build();
+
+ // Assert
+ Assert.True(actionInvoked);
+ }
+ }
+}