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:
- Aspire injects connection string
configinto the component environment - The component checks if the connection string exists
- If it does, the component adds the Azure App Configuration provider
- 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:
- Azure App Configuration (highest priority)
- Environment variables
appsettings.[ENV].jsonappsettings.json(lowest priority)
Accessing and Using the App Configuration Emulator
Accessing the GUI (Aspire Dashboard)
- Open Aspire Dashboard (usually
http://localhost:15045or similar) - Look for "config" resource in the Resources list
- 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:
Check if connection string exists:
// In component Program.cs, add logging: var appConfigConnection = builder.Configuration.GetConnectionString("config"); Console.WriteLine($"AppConfig connection: {appConfigConnection}");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:
- Ensure
.WithAppConfiguration()is called in yourAppHost.cs - Verify the component is registered through
ComponentRegistry - 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:
Verify opt-in code exists in
Program.cs:var appConfigConnection = builder.Configuration.GetConnectionString("config"); if (!string.IsNullOrEmpty(appConfigConnection)) { builder.AddAzureAppConfiguration("config"); // ← Must be present }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:
- Restart Docker Desktop
- Check for port conflicts
- 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:
Restart the component service - Easiest solution
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)); }); });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:
- Ensure
WithDataVolume()is called - Don't run
docker volume prune - 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
- Dynaplex Architecture - Overall system architecture
- Component Development Guide - Building new components
- Aspire Documentation - .NET Aspire reference
- Azure App Configuration Docs - Official Azure docs
ADRs (Architecture Decision Records)
- ADR-007: Aspire Microservices Migration - Why we use .NET Aspire for orchestration