Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.15.0" />
<PackageReference Include="Azure.Identity" Version="1.18.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -102,6 +105,80 @@ public static IConfigurationBuilder AddAzureAppConfiguration(
return configurationBuilder;
}

/// <summary>
/// Adds key-value data from an Azure App Configuration store to a configuration builder.
/// The <see cref="ConfigurationClient"/> is created from the specified configuration section using
/// <see cref="ConfigurationClientSettings"/>.
/// </summary>
/// <param name="configurationBuilder">The configuration builder to add key-values to.</param>
/// <param name="sectionName">The name of the configuration section that contains the <see cref="ConfigurationClientSettings"/>.</param>
/// <param name="optional">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.
/// <exception cref="ArgumentException"/> will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
/// </param>
/// <returns>The provided configuration builder.</returns>
/// <remarks>
/// For more information on configuring Azure clients from configuration, see
/// <see href="https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src/docs/ConfigurationAndDependencyInjection.md">Configuration and Dependency Injection</see>.
/// </remarks>
[Experimental("SCME0002")]
public static IConfigurationBuilder AddAppConfigurations(
this IConfigurationBuilder configurationBuilder,
string sectionName,
bool optional = false)
{
return AddAppConfigurations(configurationBuilder, sectionName, null, optional);
}

/// <summary>
/// Adds key-value data from an Azure App Configuration store to a configuration builder.
/// The <see cref="ConfigurationClient"/> is created from the specified configuration section using
/// <see cref="ConfigurationClientSettings"/>. The <paramref name="action"/> callback can be used
/// to further configure the provider (e.g., selecting keys, configuring refresh, using feature flags).
/// </summary>
/// <param name="configurationBuilder">The configuration builder to add key-values to.</param>
/// <param name="sectionName">The name of the configuration section that contains the <see cref="ConfigurationClientSettings"/>.</param>
/// <param name="action">An optional callback used to configure Azure App Configuration options.</param>
/// <param name="optional">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.
/// <exception cref="ArgumentException"/> will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
/// </param>
/// <returns>The provided configuration builder.</returns>
/// <remarks>
/// For more information on configuring Azure clients from configuration, see
/// <see href="https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src/docs/ConfigurationAndDependencyInjection.md">Configuration and Dependency Injection</see>.
/// </remarks>
[Experimental("SCME0002")]
public static IConfigurationBuilder AddAppConfigurations(
this IConfigurationBuilder configurationBuilder,
string sectionName,
Action<AzureAppConfigurationOptions> 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<ConfigurationClientSettings>(sectionName);
TokenCredential credential = (TokenCredential)settings.CredentialProvider;

return configurationBuilder.AddAzureAppConfiguration(options =>
{
options.Connect(settings.Endpoint, credential);
action?.Invoke(options);
}, optional);
}

/// <summary>
/// Adds Azure App Configuration services to the specified <see cref="IServiceCollection"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

#if !NET8_0_OR_GREATER

#nullable enable

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Indicates that an API is experimental and it may change in the future.
/// </summary>
[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
{
/// <summary>
/// Initializes a new instance of the <see cref="ExperimentalAttribute"/> class.
/// </summary>
/// <param name="diagnosticId">The ID that the compiler will use when reporting a use of the API the attribute applies to.</param>
public ExperimentalAttribute(string diagnosticId)
{
DiagnosticId = diagnosticId;
}

/// <summary>
/// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to.
/// </summary>
public string DiagnosticId { get; }

/// <summary>
/// Gets or sets the URL for corresponding documentation.
/// </summary>
public string? UrlFormat { get; set; }
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Data.AppConfiguration" Version="1.8.0" />
<PackageReference Include="Azure.Data.AppConfiguration" Version="1.9.0" />
<PackageReference Include="Azure.Identity" Version="1.18.0" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="5.0.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.8.0" />
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.19" />
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.12.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T>` 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<AzureAppConfigurationOptions>, bool)` — Adds App Configuration using a named configuration section with additional options configuration.

### Example Usage

```csharp
// appsettings.json
// {
// "AppConfiguration": {
// "Endpoint": "https://<your-store>.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
<PropertyGroup>
<NoWarn>$(NoWarn);SCME0002</NoWarn>
</PropertyGroup>
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.15.0" />
<PackageReference Include="Azure.Identity" Version="1.18.0" />
<PackageReference Include="Azure.ResourceManager" Version="1.13.2" />
<PackageReference Include="Azure.ResourceManager.AppConfiguration" Version="1.4.1" />
<PackageReference Include="Azure.ResourceManager.KeyVault" Version="1.3.2" />
Expand Down
151 changes: 151 additions & 0 deletions tests/Tests.AzureAppConfiguration/Unit/ConfigurationSettingsTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> CreateConfigSection()
{
return new Dictionary<string, string>
{
{ $"{SectionName}:Endpoint", TestEndpoint },
{ $"{SectionName}:Credential:CredentialSource", "AzureCli" }
};
}

[Fact]
public void AddAppConfigurationsThrowsOnNullBuilder()
{
Assert.Throws<ArgumentNullException>(() =>
AzureAppConfigurationExtensions.AddAppConfigurations(null, SectionName));
}

[Fact]
public void AddAppConfigurationsThrowsOnNullSectionName()
{
var builder = new ConfigurationBuilder();
Assert.Throws<ArgumentException>(() =>
builder.AddAppConfigurations(null));
}

[Fact]
public void AddAppConfigurationsThrowsOnEmptySectionName()
{
var builder = new ConfigurationBuilder();
Assert.Throws<ArgumentException>(() =>
builder.AddAppConfigurations(string.Empty));
}

[Fact]
public void AddAppConfigurationsWithActionThrowsOnNullBuilder()
{
Assert.Throws<ArgumentNullException>(() =>
AzureAppConfigurationExtensions.AddAppConfigurations(null, SectionName, options => { }));
}

[Fact]
public void AddAppConfigurationsWithActionThrowsOnNullSectionName()
{
var builder = new ConfigurationBuilder();
Assert.Throws<ArgumentException>(() =>
builder.AddAppConfigurations(null, options => { }));
}

[Fact]
public void AddAppConfigurationsWithActionThrowsOnEmptySectionName()
{
var builder = new ConfigurationBuilder();
Assert.Throws<ArgumentException>(() =>
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);
}
}
}
Loading