diff --git a/Aspire.sln b/Aspire.sln
index d81f37f087a..fcb47f7ddde 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -658,6 +658,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Npgsql.EntityF
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Components.Common.Tests", "tests\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj", "{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration", "src\Components\Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration\Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj", "{C33CE874-27B7-4194-A2E7-D0CD950997CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests", "tests\Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests\Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests.csproj", "{FE972BF3-F448-4CF0-8544-0056B26BF006}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Yarp", "src\Aspire.Hosting.Yarp\Aspire.Hosting.Yarp.csproj", "{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}"
@@ -3883,6 +3887,30 @@ Global
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x64.Build.0 = Release|Any CPU
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x86.ActiveCfg = Release|Any CPU
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x86.Build.0 = Release|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Debug|x64.Build.0 = Debug|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Debug|x86.Build.0 = Debug|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Release|x64.ActiveCfg = Release|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Release|x64.Build.0 = Release|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Release|x86.ActiveCfg = Release|Any CPU
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC}.Release|x86.Build.0 = Release|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Debug|x64.Build.0 = Debug|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Debug|x86.Build.0 = Debug|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Release|x64.ActiveCfg = Release|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Release|x64.Build.0 = Release|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Release|x86.ActiveCfg = Release|Any CPU
+ {FE972BF3-F448-4CF0-8544-0056B26BF006}.Release|x86.Build.0 = Release|Any CPU
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -3895,7 +3923,7 @@ Global
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x64.Build.0 = Release|Any CPU
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x86.ActiveCfg = Release|Any CPU
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x86.Build.0 = Release|Any CPU
- {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x64.ActiveCfg = Debug|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x64.Build.0 = Debug|Any CPU
@@ -4319,6 +4347,8 @@ Global
{192747A2-9338-DECF-5C8C-28EB8E13829B} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C}
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C}
+ {C33CE874-27B7-4194-A2E7-D0CD950997CC} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
+ {FE972BF3-F448-4CF0-8544-0056B26BF006} = {C424395C-1235-41A4-BF55-07880A04368C}
{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {77CFE74A-32EE-400C-8930-5025E8555256}
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941} = {77CFE74A-32EE-400C-8930-5025E8555256}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 08ea0b6de79..7a3760073a9 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,6 +25,7 @@
+
diff --git a/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
new file mode 100644
index 00000000000..689d146e3c5
--- /dev/null
+++ b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -0,0 +1,31 @@
+
+
+
+ $(DefaultTargetFramework)
+ true
+ $(ComponentAzurePackageTags) configuration appconfiguration
+ A client for Azure App Configuration that integrates with Aspire.
+ $(SharedDir)AzureAppConfig_256x.png
+ $(NoWarn);SYSLIB1100;SYSLIB1101
+
+ false
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AspireAppConfigurationExtensions.cs b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AspireAppConfigurationExtensions.cs
new file mode 100644
index 00000000000..803d90e466a
--- /dev/null
+++ b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AspireAppConfigurationExtensions.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Azure.Identity;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Aspire.Azure.Common;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for registering and configuring Azure App Configuration in a .NET Aspire application.
+///
+public static class AspireAppConfigurationExtensions
+{
+ internal const string DefaultConfigSectionName = "Aspire:Microsoft:Extensions:Configuration:AzureAppConfiguration";
+
+ ///
+ /// Adds the Azure App Configuration to be configuration in the .
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ /// An optional method that can be used for customizing the .
+ /// Reads the settings from "Aspire:Microsoft:Extensions:Configuration:AzureAppConfiguration" section.
+ /// Thrown when mandatory is not provided.
+ public static void AddAzureAppConfiguration(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null,
+ Action? configureOptions = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(connectionName);
+
+ IConfigurationSection configSection = builder.Configuration.GetSection(DefaultConfigSectionName);
+
+ var settings = new AzureAppConfigurationSettings();
+ configSection.Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ ((IConnectionStringSettings)settings).ParseConnectionString(connectionString);
+ }
+
+ configureSettings?.Invoke(settings);
+
+ if (settings.Endpoint is null)
+ {
+ throw new InvalidOperationException($"Endpoint is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'Endpoint' key in the '{DefaultConfigSectionName}' configuration section.");
+ }
+
+ builder.Configuration.AddAzureAppConfiguration(
+ options =>
+ {
+ options.Connect(settings.Endpoint, settings.Credential ?? new DefaultAzureCredential());
+ configureOptions?.Invoke(options);
+ },
+ settings.Optional);
+
+ builder.Services.AddAzureAppConfiguration(); // register IConfigurationRefresherProvider service
+ }
+}
diff --git a/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs
new file mode 100644
index 00000000000..1021be2c2ab
--- /dev/null
+++ b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration;
+
+[assembly: ConfigurationSchema("Aspire:Microsoft:Extensions:Configuration:AzureAppConfiguration", typeof(AzureAppConfigurationSettings))]
+
+[assembly: LoggingCategories(
+ "Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh")]
diff --git a/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSettings.cs b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSettings.cs
new file mode 100644
index 00000000000..446639687a9
--- /dev/null
+++ b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSettings.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Azure.Common;
+using Azure.Core;
+
+namespace Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration;
+
+///
+/// Provides the client configuration settings for connecting to Azure App Configuration.
+///
+public sealed class AzureAppConfigurationSettings : IConnectionStringSettings
+{
+ ///
+ /// A to the App Configuration store on which the client operates. Appears as "Endpoint" in the Azure portal.
+ /// This is likely to be similar to "https://{store_name}.azconfig.io".
+ ///
+ public Uri? Endpoint { get; set; }
+
+ ///
+ /// Gets or sets the credential used to authenticate to the Azure App Configuration.
+ ///
+ public TokenCredential? Credential { get; set; }
+
+ ///
+ /// 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 configuration values are populated from Azure App Configuration.
+ ///
+ public bool Optional { get; set; }
+
+ void IConnectionStringSettings.ParseConnectionString(string? connectionString)
+ {
+ if (!string.IsNullOrEmpty(connectionString) &&
+ Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
+ {
+ Endpoint = uri;
+ }
+ }
+}
diff --git a/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSchema.json b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSchema.json
new file mode 100644
index 00000000000..248a28ed985
--- /dev/null
+++ b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationSchema.json
@@ -0,0 +1,49 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ },
+ "type": "object",
+ "properties": {
+ "Aspire": {
+ "type": "object",
+ "properties": {
+ "Microsoft": {
+ "type": "object",
+ "properties": {
+ "Extensions": {
+ "type": "object",
+ "properties": {
+ "Configuration": {
+ "type": "object",
+ "properties": {
+ "AzureAppConfiguration": {
+ "type": "object",
+ "properties": {
+ "Endpoint": {
+ "type": "string",
+ "format": "uri",
+ "description": "A 'System.Uri' to the App Configuration store on which the client operates. Appears as \"Endpoint\" in the Azure portal. This is likely to be similar to \"https://{store_name}.azconfig.io\"."
+ },
+ "Optional": {
+ "type": "boolean",
+ "description": "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 configuration values are populated from Azure App Configuration."
+ }
+ },
+ "description": "Provides the client configuration settings for connecting to Azure App Configuration."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/README.md b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/README.md
new file mode 100644
index 00000000000..2038a3be9e8
--- /dev/null
+++ b/src/Components/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration/README.md
@@ -0,0 +1,165 @@
+# Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration
+
+Retrieves configuration settings from Azure App Configuration to use in your application. Registers Azure App Configuration service as a configuration source. Enables corresponding logging.
+
+## Getting started
+
+### Prerequisites
+
+- Azure subscription - [create one for free](https://azure.microsoft.com/free/)
+- Azure App Configuration - [create one](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-azure-app-configuration-create).
+
+### Install the package
+
+Install the .NET Aspire Azure App Configuration library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration
+```
+
+## Usage examples
+
+### Add App Configuration to configuration
+
+In the _Program.cs_ file of your project, call the `builder.Configuration.AddAzureAppConfiguration` extension method to add key-values from Azure App Configuration to the application's Configuration. The method takes a connection name parameter.
+
+```csharp
+builder.AddAzureAppConfiguration("appConfig");
+```
+
+You can then retrieve a key-value through normal `IConfiguration` APIS. For example, to retrieve a key-value from a Web API controller:
+
+```csharp
+public MyController(IConfiguration configuration)
+{
+ string someValue = configuration["someKey"];
+}
+```
+
+#### Use feature flags
+
+To use feature flags, install the Feature Management library:
+
+```dotnetcli
+dotnet add package Microsoft.FeatureManagement
+```
+
+App Configuration will not load feature flags by default. To load feature flags, you can pass the `Action configureOptions` delegate when calling `builder.AddAzureAppConfiguration`.
+
+```csharp
+builder.AddAzureAppConfiguration(
+ "appConfig",
+ configureOptions: options => options.UseFeatureFlags());
+
+// Register feature management services
+builder.Services.AddFeatureManagement();
+```
+
+You can then use `IVariantFeatureManager` to evaluate feature flags in your application:
+
+```csharp
+private readonly IVariantFeatureManager _featureManager;
+
+public MyController(IVariantFeatureManager featureManager)
+{
+ _featureManager = featureManager;
+}
+
+[HttpGet]
+public async Task Get()
+{
+ if (await _featureManager.IsEnabledAsync("NewFeature"))
+ {
+ return Ok("New feature is enabled!");
+ }
+
+ return Ok("Using standard implementation.");
+}
+```
+
+For information about using the Feature Management library, please go to the [documentation](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-dotnet-reference).
+
+## Configuration
+
+The .NET Aspire Azure App Configuration library provides multiple options to configure the Azure App Configuration connection based on the requirements and conventions of your project. Note that the App Config endpoint is required to be supplied, either in `AzureAppConfigurationSettings.Endpoint` or using a connection string.
+
+### Use a connection string
+
+When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureAppConfiguration()`:
+
+```csharp
+builder.AddAzureAppConfiguration("appConfigConnectionName");
+```
+
+And then the App Configuration endpoint will be retrieved from the `ConnectionStrings` configuration section. The App Configuration store URI works with the `AzureAppConfigurationSettings.Credential` property to establish a connection. If no credential is configured, the [DefaultAzureCredential](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential) is used.
+
+```json
+{
+ "ConnectionStrings": {
+ "appConfigConnectionName": "https://{store_name}.azconfig.io"
+ }
+}
+```
+
+### Use configuration providers
+
+The .NET Aspire Azure App Configuration library supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `AzureAppConfigurationSettings` from configuration by using the `Aspire:Microsoft:Extensions:Configuration:AzureAppConfiguration` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration
+ "Aspire": {
+ "Microsoft": {
+ "Extensions": {
+ "Configuration": {
+ "AzureAppConfiguration": {
+ "Endpoint": "YOUR_APPCONFIGURATION_ENDPOINT_URI"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+### Use inline delegates
+
+You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set App Configuration endpoint from code:
+
+```csharp
+builder.AddAzureAppConfiguration("appConfig", configureSettings: settings => settings.Endpoint = "http://YOUR_URI");
+```
+
+## AppHost extensions
+
+In your AppHost project, install the Aspire Azure App Configuration Hosting library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Hosting.Azure.AppConfiguration
+```
+
+Then, in the _Program.cs_ file of `AppHost`, add a App Configuration connection and consume the connection using the following methods:
+
+```csharp
+// Service registration
+var appConfig = builder.AddAzureAppConfiguration("appConfig");
+
+// Service consumption
+var myService = builder.AddProject()
+ .WithReference(appConfig);
+```
+
+The `AddAzureAppConfiguration` method adds an Azure App Configuration resource to the builder. Or `AddConnectionString` can be used to read connection information from the AppHost's configuration under the `ConnectionStrings:appConfig` config key. The `WithReference` method passes that connection information into a connection string named `appConfig` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using:
+
+```csharp
+builder.AddAzureAppConfiguration("appConfig");
+```
+
+## Additional documentation
+
+* https://learn.microsoft.com/azure/azure-app-configuration/
+* https://github.com/dotnet/aspire/tree/main/src/Components/README.md
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md
index 417820ff8c8..752180d6da0 100644
--- a/src/Components/Aspire_Components_Progress.md
+++ b/src/Components/Aspire_Components_Progress.md
@@ -10,6 +10,7 @@ These integrations should follow the [.NET Aspire Integration Requirements](#net
| Microsoft.Data.SqlClient | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
| Microsoft.EntityFramework.Cosmos | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Microsoft.EntityFrameworkCore.SqlServer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
+| Microsoft.Extensions.Configuration.AzureAppConfiguration | ✅ | ✅ | ✅ | ✅ | ✅ | | ❌ | |
| MongoDB.Driver | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Azure.AI.OpenAI | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Azure.Data.Tables | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md
index c19559187e9..1d0b979517c 100644
--- a/src/Components/Telemetry.md
+++ b/src/Components/Telemetry.md
@@ -181,6 +181,14 @@ Aspire.Microsoft.EntityFrameworkCore.SqlServer:
- Metric names:
- none
+Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration
+- Log categories:
+ "Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh"
+- Activity source names:
+ - none
+- Metric names:
+ - none
+
Aspire.Milvus.Client:
- Log categories:
"Milvus.Client"
diff --git a/tests/Aspire.Components.Common.Tests/ConformanceTests.cs b/tests/Aspire.Components.Common.Tests/ConformanceTests.cs
index 3415faf1fcb..117b77a8f0c 100644
--- a/tests/Aspire.Components.Common.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Components.Common.Tests/ConformanceTests.cs
@@ -45,6 +45,8 @@ public abstract class ConformanceTests
protected virtual bool SupportsKeyedRegistrations => false;
+ protected virtual bool IsComponentBuiltBeforeHost => false;
+
protected bool MetricsAreSupported => CheckIfImplemented(SetMetrics);
// every Component has to support health checks, this property is a temporary workaround
@@ -364,6 +366,8 @@ public void ConfigurationSchemaInvalidJsonConfigTest()
[InlineData(false)]
public void ConnectionInformationIsDelayValidated(bool useKey)
{
+ SkipIfComponentIsBuiltBeforeHost();
+
SetupConnectionInformationIsDelayValidated();
var builder = Host.CreateEmptyApplicationBuilder(null);
@@ -544,6 +548,14 @@ protected void SkipIfNamedConfigNotSupported()
public static string CreateConfigKey(string prefix, string? key, string suffix)
=> string.IsNullOrEmpty(key) ? $"{prefix}:{suffix}" : $"{prefix}:{key}:{suffix}";
+ protected void SkipIfComponentIsBuiltBeforeHost()
+ {
+ if (IsComponentBuiltBeforeHost)
+ {
+ Assert.Skip("Component is built before host.");
+ }
+ }
+
protected HostApplicationBuilder CreateHostBuilder(HostApplicationBuilderSettings? hostSettings = null, string? key = null)
{
HostApplicationBuilder builder = Host.CreateEmptyApplicationBuilder(hostSettings);
diff --git a/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/AppConfiguratinPublicApiTests.cs b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/AppConfiguratinPublicApiTests.cs
new file mode 100644
index 00000000000..ab6f9a7ebf0
--- /dev/null
+++ b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/AppConfiguratinPublicApiTests.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests;
+
+public class AppConfigurationPublicApiTests
+{
+ [Fact]
+ public void AddAzureAppConfigurationShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+ const string connectionName = "appConfig";
+
+ var action = () => builder.AddAzureAppConfiguration(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddAzureAppConfigurationShouldThrowWhenConnectionNameIsNullOrEmpty(bool isNull)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ var connectionName = isNull ? null! : string.Empty;
+
+ var action = () => builder.AddAzureAppConfiguration(connectionName);
+
+ var exception = isNull
+ ? Assert.Throws(action)
+ : Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+}
diff --git a/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests.csproj b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests.csproj
new file mode 100644
index 00000000000..e76ed140c1d
--- /dev/null
+++ b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ $(DefaultTargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/AspireAppConfigurationExtensionsTest.cs b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/AspireAppConfigurationExtensionsTest.cs
new file mode 100644
index 00000000000..6db1a5e8cd0
--- /dev/null
+++ b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/AspireAppConfigurationExtensionsTest.cs
@@ -0,0 +1,121 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Azure;
+using Azure.Core;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using System.Text;
+using Xunit;
+
+namespace Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests;
+
+public class AspireAppConfigurationExtensionsTest
+{
+ [Fact]
+ public void AppConfigEndpointCanBeSetInCode()
+ {
+ var endpoint = new Uri(ConformanceTests.Endpoint);
+ var mockTransport = new MockTransport(CreateResponse("""{}"""));
+
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:appConfig", "https://unused.azconfig.io/")
+ ]);
+
+ builder.AddAzureAppConfiguration(
+ "appConfig",
+ settings => {
+ settings.Endpoint = endpoint;
+ settings.Credential = new EmptyTokenCredential();
+ },
+ options => options.ConfigureClientOptions(clientOptions => clientOptions.Transport = mockTransport));
+
+ Assert.NotEmpty(mockTransport.Requests);
+ var request = mockTransport.Requests[0];
+ Assert.StartsWith(endpoint.ToString(), request.Uri.ToString());
+ }
+
+ [Fact]
+ public void ConnectionNameWinsOverConfiguration()
+ {
+ var endpoint = new Uri(ConformanceTests.Endpoint);
+ var mockTransport = new MockTransport(CreateResponse("""{}"""));
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:Microsoft:Extensions:Configuration:AzureAppConfiguration", null, "Endpoint"), "https://unused.azconfig.io/"),
+ new KeyValuePair("ConnectionStrings:appConfig", ConformanceTests.Endpoint)
+ ]);
+
+ builder.AddAzureAppConfiguration(
+ "appConfig",
+ settings =>
+ {
+ settings.Endpoint = endpoint;
+ settings.Credential = new EmptyTokenCredential();
+ },
+ options => options.ConfigureClientOptions(clientOptions => clientOptions.Transport = mockTransport));
+
+ Assert.NotEmpty(mockTransport.Requests);
+ var request = mockTransport.Requests[0];
+ Assert.StartsWith(endpoint.ToString(), request.Uri.ToString());
+ }
+
+ [Fact]
+ public void AddsAppConfigurationToApplication()
+ {
+ var endpoint = new Uri("https://aspiretests.azconfig.io/");
+ var mockTransport = new MockTransport(CreateResponse("""
+ {
+ "items": [
+ {
+ "key": "test-key-1",
+ "value": "test-value-1"
+ },
+ {
+ "key": "test-key-2",
+ "value": "test-value-2"
+ }
+ ]
+ }
+ """));
+
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.AddAzureAppConfiguration(
+ "appConfig",
+ settings =>
+ {
+ settings.Endpoint = endpoint;
+ settings.Credential = new EmptyTokenCredential();
+ },
+ options => options.ConfigureClientOptions(clientOptions => clientOptions.Transport = mockTransport));
+
+ Assert.Equal("test-value-1", builder.Configuration["test-key-1"]);
+ Assert.Equal("test-value-2", builder.Configuration["test-key-2"]);
+ }
+
+ private static MockResponse CreateResponse(string content)
+ {
+ var buffer = Encoding.UTF8.GetBytes(content);
+ var response = new MockResponse(200)
+ {
+ ClientRequestId = Guid.NewGuid().ToString(),
+ ContentStream = new MemoryStream(buffer),
+ };
+
+ return response;
+ }
+
+ internal sealed class EmptyTokenCredential : TokenCredential
+ {
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken(string.Empty, DateTimeOffset.MaxValue);
+ }
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue));
+ }
+ }
+}
diff --git a/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/ConfigurationTests.cs b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/ConfigurationTests.cs
new file mode 100644
index 00000000000..49ee121899f
--- /dev/null
+++ b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/ConfigurationTests.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void EndpointUriIsNullByDefault()
+ => Assert.Null(new AzureAppConfigurationSettings().Endpoint);
+
+ // WIP: https://github.com/Azure/AppConfiguration-DotnetProvider/pull/645
+ // Tracing will be supported in the next 8.2.0 release
+}
diff --git a/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/ConformanceTests.cs b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/ConformanceTests.cs
new file mode 100644
index 00000000000..dffa9c1c517
--- /dev/null
+++ b/tests/Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests/ConformanceTests.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.ConformanceTests;
+using Azure.Core;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using System.Reflection;
+using Xunit;
+
+namespace Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration.Tests;
+
+public class ConformanceTests : ConformanceTests
+{
+ public const string Endpoint = "https://aspiretests.azconfig.io/";
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ protected override string ActivitySourceName => "Microsoft.Extensions.Configuration.AzureAppConfiguration";
+
+ protected override string[] RequiredLogCategories => new string[] { "Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh" };
+
+ protected override bool SupportsKeyedRegistrations => false;
+
+ protected override bool IsComponentBuiltBeforeHost => true;
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Azure": {
+ "AppConfiguration": {
+ "Endpoint": "http://YOUR_URI",
+ "Optional": true
+ }
+ }
+ }
+ }
+ """;
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ => configuration.AddInMemoryCollection(new KeyValuePair[]
+ {
+ new(CreateConfigKey("Aspire:Microsoft:Extensions:Configuration:AzureAppConfiguration", null, "Endpoint"), Endpoint)
+ });
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ builder.AddAzureAppConfiguration(
+ "appconfig",
+ settings =>
+ {
+ configure?.Invoke(settings);
+ settings.Credential = new EmptyTokenCredential();
+ settings.Optional = true;
+ },
+ options =>
+ {
+ // AzureAppConfigurationOptions.MinBackoffDuration is internal, use reflection to set it to 1 second to facilitate testing.
+ var minBackoffDurationProperty = options.GetType().GetProperty("MinBackoffDuration", BindingFlags.Instance | BindingFlags.NonPublic);
+ minBackoffDurationProperty?.SetValue(options, TimeSpan.FromSeconds(1));
+
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("sentinel")
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
+ });
+ options.ConfigureStartupOptions(startupOptions =>
+ {
+ startupOptions.Timeout = TimeSpan.FromSeconds(1);
+ });
+ options.ConfigureClientOptions(clientOptions => clientOptions.Retry.MaxRetries = 0);
+ });
+ }
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "Microsoft": { "Extensions": { "Configuration": { "AzureAppConfiguration": { "Endpoint": "YOUR_URI"}}}}}}""", "Value does not match format \"uri\""),
+ ("""{"Aspire": { "Microsoft": { "Extensions": { "Configuration": { "AzureAppConfiguration": { "Endpoint": "http://YOUR_URI", "Optional": "true"}}}}}}""", "Value is \"string\" but should be \"boolean\"")
+ };
+
+ protected override void SetHealthCheck(AzureAppConfigurationSettings options, bool enabled)
+ // WIP: https://github.com/Azure/AppConfiguration-DotnetProvider/pull/644
+ => throw new NotImplementedException();
+
+ protected override void SetMetrics(AzureAppConfigurationSettings options, bool enabled)
+ => throw new NotImplementedException();
+
+ protected override void SetTracing(AzureAppConfigurationSettings options, bool enabled)
+ // WIP: https://github.com/Azure/AppConfiguration-DotnetProvider/pull/645
+ // Will be supported in the next 8.2.0 release
+ => throw new NotImplementedException();
+
+ protected override void TriggerActivity(IConfigurationRefresherProvider service)
+ // WIP: https://github.com/Azure/AppConfiguration-DotnetProvider/pull/645
+ // Will be supported in the next 8.2.0 release
+ => throw new NotImplementedException();
+
+ [Fact]
+ public void TracingEnablesTheRightActivitySource()
+ // WIP: Waiting for App Configuration Provider 8.2.0 release
+ => RemoteExecutor.Invoke(() => /*ActivitySourceTest(key: null)*/ null).Dispose();
+
+ internal sealed class EmptyTokenCredential : TokenCredential
+ {
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken(string.Empty, DateTimeOffset.MaxValue);
+ }
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue));
+ }
+ }
+}