Documentation

how-to/azure-app-configuration.md


title: Azure App Configuration in Dynaplex author: Daniel Castonguay last-updated: 2025-10-20 type: explanation audience: Developers working with Dynaplex components reviewed: true

Overview

Instead of having to deal with 4 trillion appsettings.json files all over the repo, and having to keep track of how each file overrides the other, you can instead:

  • Put everything in one place
  • Have everything stay where it's supposed to
  • Have an easy interface for managing all configuration across any deployed instance

This is what happens when you migrate from appsettings.json to using the dplx configuration:

  • During local development:, the Azure App Configuration emulator will run on your machine. You'll get a web interface where you can see all your config parameters across every component and you'll be able to edit them or add new ones.
  • When the code is deployed to Azure, the real Azure App Configuration service is used
  • All config data persists across container restarts via Docker volumes in dev, and in Azure the App Configuration resource has all of it's own persistence controls.

This document explains how App Configuration integrates with the Dynaplex architecture and how to use it effectively.

Architecture

Infrastructure

The App Configuration emulator is provisioned at the infrastructure level by the InfrastructureBuilder, so you never really have to worry about that part of the equation; the complexity is hidden by the orchestration and base layers. That said, it's not terribly complex, and the bulk of the work is inside of the WithAppConfiguration() method:

// strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/InfrastructureBuilder.cs
public InfrastructureBuilder WithAppConfiguration() {
    if (_isAzurePublish) {
        // Azure deployment: provision real Azure App Configuration
        AppConfig = _builder.AddAzureAppConfiguration("config")
            .ConfigureInfrastructure(infra => {
                var appConfigStore = infra.GetProvisionableResources()
                    .OfType<AppConfigurationStore>().Single();
                appConfigStore.SkuName = "developer";
            });
    } else {
        // Local development: use emulator with persisted data
        AppConfig = _builder.AddAzureAppConfiguration("config")
            .ConfigureInfrastructure(infra => {
                var appConfigStore = infra.GetProvisionableResources()
                    .OfType<AppConfigurationStore>().Single();
                appConfigStore.SkuName = "developer";
            })
            .RunAsEmulator(settings => {
                settings.WithDataVolume();  // Persist config between runs
            });
    }

    return this;
}

Component Integration

Any developer maintaining a component that is currently using appsettings.json or any other method of configuration can easily opt-in and start using the app configuration by simply modifying the Program.cs of the component with the following line:
Components opt-in to App Configuration in their Program.cs:

// Program.cs
var appConfigConnection = builder.Configuration.GetConnectionString("config");
if (!string.IsNullOrEmpty(appConfigConnection)) {
	builder.AddAzureAppConfiguration("config");
}

When you do this, the following things happen:

  1. Aspire injects connection string config into the component environment
  2. The component checks if the connection string exists
  3. If it does, the component adds the Azure App Configuration provider
  4. If this fails, the component fails back to appsettings.json

Configuration Precedence

When a component uses App Configuration, settings are read in the following order:

  1. Azure App Configuration (highest priority)
  2. Environment variables
  3. appsettings.[ENV].json
  4. appsettings.json (lowest priority)

Accessing and Using the App Configuration Emulator

Accessing the GUI (Aspire Dashboard)

  1. Open Aspire Dashboard (usually http://localhost:15045 or similar)
  2. Look for "config" resource in the Resources list
  3. Click on the endpoint to open the emulator UI

Direct Access

You can also access the emulator directly through the dynamic port assigned by Aspire.

Adding and Updating Configuration Values

In the emulator UI:

Key:    Mercury:EventHubBridge:Name
Value:  production-event-hub

Key naming conventions:

Use colon (:) as the hierarchy separator, matching your appsettings.json structure:

appsettings.json App Configuration Key
"ConnectionStrings": { "Database": "..." } ConnectionStrings:Database
"Mercury": { "EventHubBridge": { "Name": "..." } } Mercury:EventHubBridge:Name
"Logging": { "LogLevel": { "Default": "..." } } Logging:LogLevel:Default

Retrieving Data from App Configuration

There is no code change required if you're using the standard configuration API. Just do what you would normally do:

// Works the same whether config comes from appsettings.json or App Configuration
var eventHubName = builder.Configuration["Mercury:EventHubBridge:Name"];

// Or with strongly-typed options
builder.Services.Configure<EventHubBridgeConfiguration>(
    builder.Configuration.GetSection("Mercury:EventHubBridge")
);

Data Persistence

Local Development

The emulator uses a Docker volume to persist configuration:

Volume lifecycle:

  • Persists across: Container restarts, app restarts, system reboots
  • Lost when: You explicitly delete the volume or run docker volume prune

To view volumes:

docker volume ls | grep app-configuration

To inspect a volume:

docker volume inspect <volume-name>

Azure Deployment

In Azure, App Configuration is a fully managed service with:

  • Persistence: Built-in, durable storage
  • Backup: Point-in-time snapshots available
  • Replication: Geo-redundant storage
  • History: Configuration change history tracking

Troubleshooting

Component Can't Connect to App Configuration

Symptoms:

  • Component starts but doesn't read App Configuration values
  • No errors in logs

Diagnosis:

  1. Check if connection string exists:

    // In component Program.cs, add logging:
    var appConfigConnection = builder.Configuration.GetConnectionString("config");
    Console.WriteLine($"AppConfig connection: {appConfigConnection}");
    
  2. Check Aspire dashboard:

    • Is "config" resource running?
    • Does it show endpoints?
    • View component environment variables - look for ConnectionStrings__config

Solution:

The connection string is automatically injected by the infrastructure. If it's missing:

  1. Ensure .WithAppConfiguration() is called in your AppHost.cs
  2. Verify the component is registered through ComponentRegistry
  3. Check that the App Configuration emulator/service is running

App Configuration Values Not Loading

Symptoms:

  • Component reads from appsettings.json instead of App Configuration
  • Values in emulator are ignored

Diagnosis:

  1. Verify opt-in code exists in Program.cs:

    var appConfigConnection = builder.Configuration.GetConnectionString("config");
    if (!string.IsNullOrEmpty(appConfigConnection))
    {
        builder.AddAzureAppConfiguration("config");  // ← Must be present
    }
    
  2. Check configuration provider order:

    // Add temporary logging
    foreach (var source in builder.Configuration.Sources)
    {
        Console.WriteLine($"Config source: {source.GetType().Name}");
    }
    // Should include: AzureAppConfigurationProvider
    

Solution:

Add the builder.AddAzureAppConfiguration("config") call in your component's Program.cs.

Emulator Container Not Starting

Symptoms:

  • App Configuration resource shows as unhealthy in Aspire dashboard
  • Components fail to start with connection errors

Diagnosis:

# Check container status
docker ps -a | grep app-configuration

# Check container logs
docker logs <container-id>

Common causes:

  • Port conflict (another service using the emulator's port)
  • Docker daemon not running
  • Insufficient Docker resources

Solution:

  1. Restart Docker Desktop
  2. Check for port conflicts
  3. Increase Docker memory allocation (4GB+ recommended)

Configuration Changes Not Reflected

Symptoms:

  • Changed values in App Configuration emulator
  • Component still uses old values

Diagnosis:

App Configuration provider caches values by default.

Solution:

  1. Restart the component service - Easiest solution

  2. Configure refresh - Add watch keys:

    builder.AddAzureAppConfiguration(options => {
        options.Connect(builder.Configuration.GetConnectionString("config"))
               .ConfigureRefresh(refresh => {
                   refresh.Register("Sentinel", refreshAll: true)
                          .SetCacheExpiration(TimeSpan.FromSeconds(30));
               });
    });
    
  3. Use sentinel key - Change a "Sentinel" key to trigger refresh

Data Lost After Restart

Symptoms:

  • Configuration disappears when restarting Aspire app
  • Have to re-enter values every time

Diagnosis:

Data volume not configured or deleted.

Verification:

// Should be present in InfrastructureBuilder
.RunAsEmulator(settings => {
    settings.WithDataVolume();  // ← Must be present
});

Solution:

  1. Ensure WithDataVolume() is called
  2. Don't run docker volume prune
  3. Check Docker Desktop settings - volumes should not auto-delete

Best Practices

1. Use Hierarchical Keys

Organize configuration with logical hierarchy:

✅ Good:
Mercury:EventHubBridge:Name
Mercury:EventHubBridge:ConnectionString
Mercury:OctaneIntegration:Name

❌ Bad:
mercury_eventhub_name
mercury_eventhub_connection
octane_name

Benefits:

  • Maps cleanly to appsettings.json structure
  • Easy to bind to strongly-typed options classes
  • Clear ownership and grouping

2. Keep Secrets Out of App Configuration

App Configuration is for configuration, not secrets.

For secrets, use:

  • Azure Key Vault (production)
  • User Secrets (dotnet user-secrets) (local development)
  • Environment variables (container environments)

Example:

✅ App Configuration:
Mercury:EventHubBridge:Name = "shared-hub"
Mercury:EventHubBridge:TimeoutSeconds = "30"

✅ Key Vault / User Secrets:
Mercury:EventHubBridge:ConnectionString = "Endpoint=sb://..."
ConnectionStrings:Acsis = "Host=...;Password=..."

3. Use Labels for Environments

Avoid key duplication by using labels:

Key:    DatabaseServer
Label:  Development
Value:  localhost

Key:    DatabaseServer
Label:  Production
Value:  prod-sql.database.windows.net

Load with label filter:

.Select(KeyFilter.Any, builder.Environment.EnvironmentName)

4. Document Your Configuration

Create a reference toml document listing the App Configuration for the component, using the following format:

# Acsis Configuration Specification

component = "catalog" # the name of the component
strongly_typed = true # if config gets bound to class

# after this, you can nest every grouping of variables into toml groups
[FileService]
Options = [
	{
		Name = "MaxFileSizeInBytes",
		Type = "int64",
		Description = "Maximum allowed file size for upload, in bytes"
	},
	{
		Name = "UseInMemoryDatabase",
		Type = "bool",
		Description = "Whether or not to use in-memory database for storing the file uploads. Used only in development."
	}
]
[FileService.AzBlobService]
Options = [

]

[FileService.DirectoryService]
Options = [

]

[FileService.FileTypesSupported]
Options = [

]



Keep this in version control alongside your code.

Add Integration Tests for Configuration Loading

Add integration tests to verify configuration:

[Fact]
public void Configuration_LoadsFromAppConfiguration()
{
    // Arrange
    var builder = WebApplication.CreateBuilder();
    builder.AddAzureAppConfiguration("config");

    // Act
    var value = builder.Configuration["Mercury:EventHubBridge:Name"];

    // Assert
    Assert.NotNull(value);
    Assert.NotEmpty(value);
}

Related Documentation

ADRs (Architecture Decision Records)

  • ADR-007: Aspire Microservices Migration - Why we use .NET Aspire for orchestration