Best Practices for managing application and environment-specific variables for .NET 9.0 C# 12 Azure Functions (Isolated Worker model), covering local development and Azure DevOps deployments across multiple environments.
Core Principles:
- Configuration Providers: Leverage the standard .NET configuration system (
Microsoft.Extensions.Configuration). - Hierarchy of Configuration: Understand that settings can be overridden (e.g., environment variables override
appsettings.json). - Secrets Management: Use Azure Key Vault for all secrets in Azure. For local development, use User Secrets or
local.settings.json(with caution for secrets). - Environment Consistency: Aim to use the same configuration mechanisms locally and in Azure as much as possible.
- Infrastructure as Code (IaC) & DevOps: Manage Azure resources and deployments through pipelines.
Configuration Sources Order (Typical for Isolated Worker):
appsettings.json(shared defaults)appsettings.{EnvironmentName}.json(e.g.,appsettings.Development.json)- User Secrets (local development, overrides
appsettings.*.json) local.settings.json(specifically for Azure Functions local development, itsValuesmap to environment variables)- Environment Variables (OS level, or Azure App Settings in the cloud - these override all previous file-based settings)
- Azure Key Vault (via Key Vault references in App Settings, effectively acting as secure environment variables)
- Command-line arguments (less common for Functions).
Step-by-Step Guide:
Phase 1: Project Setup & Local Development
-
Create Azure Function Project (Isolated Worker):
- Use Visual Studio or the .NET CLI:
dotnet new func --isolated-worker --target-framework net9.0 -n MyFunctionApp cd MyFunctionApp
- Use Visual Studio or the .NET CLI:
-
Install Necessary NuGet Packages: The isolated worker SDK usually brings in
Microsoft.Extensions.Configurationbasics. If you need more specific providers:dotnet add package Microsoft.Extensions.Configuration.Json dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions dotnet add package Microsoft.Extensions.DependencyInjection # Usually already there # For User Secrets (local dev) dotnet add package Microsoft.Extensions.Configuration.UserSecrets
-
Configure
Program.csfor Rich Configuration: Modify yourProgram.csto build a configuration that includesappsettings.json, environment-specificappsettings.{Environment}.json, User Secrets (for local dev), and environment variables.// Program.cs using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System.IO; // Required for Path.GetDirectoryName and Assembly var host = new HostBuilder() .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; // IHostEnvironment var appAssembly = System.Reflection.Assembly.GetExecutingAssembly().Location; config.SetBasePath(Path.GetDirectoryName(appAssembly)) // Crucial for Functions .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment()) { // For local development, User Secrets are a good practice for sensitive data // not suitable for local.settings.json if it's ever committed by mistake. config.AddUserSecrets<Program>(optional: true); } // local.settings.json values are automatically loaded as environment variables // by the Azure Functions Host when running locally. // Environment variables (including those from local.settings.json and Azure App Settings) // are added by default by ConfigureFunctionsWorkerDefaults or ConfigureFunctionsWebApplication // and will override appsettings files. // You can explicitly add them too if needed: config.AddEnvironmentVariables(); }) .ConfigureFunctionsWorkerDefaults() // Or .ConfigureFunctionsWebApplication() if using ASP.NET Core integration .ConfigureServices((hostContext, services) => { // Get configuration instance IConfiguration configuration = hostContext.Configuration; // Option 1: Register strongly-typed settings (Best Practice) services.Configure<MyApplicationSettings>(configuration.GetSection("MyApplication")); services.Configure<MyEnvironmentSpecificSettings>(configuration.GetSection("EnvironmentSpecific")); // Option 2: Register IConfiguration directly if needed (less type-safe) // services.AddSingleton(configuration); // Register your services services.AddSingleton<IMyService, MyService>(); // ... other services }) .Build(); host.Run(); // Define your settings classes public class MyApplicationSettings { public string? ApiKey { get; set; } public string? ServiceUrl { get; set; } public int DefaultTimeoutSeconds { get; set; } } public class MyEnvironmentSpecificSettings { public string? DatabaseConnectionString { get; set; } public string? StorageAccountName { get; set; } }
- Note on
SetBasePath: For Azure Functions, especially when deployed, setting the base path explicitly to the assembly's directory ensuresappsettings.jsonfiles are found correctly.Directory.GetCurrentDirectory()can be unreliable in the Azure Functions runtime environment.
- Note on
-
Create
appsettings.json: This file contains default or shared settings.// appsettings.json { "MyApplication": { "ServiceUrl": "https://default.api.example.com", "DefaultTimeoutSeconds": 30 }, "EnvironmentSpecific": { "StorageAccountName": "commondatastorage" }, "Logging": { // Example, can be configured here too "LogLevel": { "Default": "Information", "Microsoft.Hosting.Lifetime": "Information" } } }
Ensure "Copy to Output Directory" is set to "Copy if newer" or "Copy always" for these JSON files in their properties in Visual Studio, or via the
.csprojfile:<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Update="appsettings.Development.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <DependentUpon>appsettings.json</DependentUpon> </None> <None Update="appsettings.Production.json"> <!-- Or UAT, Test etc. --> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <DependentUpon>appsettings.json</DependentUpon> </None> </ItemGroup>
-
Create
appsettings.Development.json: This overrides settings for the "Development" environment.// appsettings.Development.json { "MyApplication": { "ServiceUrl": "https://dev.api.example.com" }, "EnvironmentSpecific": { "DatabaseConnectionString": "DevelopmentDB_ConnectionString", // Still better in User Secrets or local.settings.json "StorageAccountName": "devdatastorage" } }
-
Set up User Secrets (for sensitive local dev data):
- Right-click the project in Visual Studio -> "Manage User Secrets". Or via CLI:
dotnet user-secrets init dotnet user-secrets set "MyApplication:ApiKey" "MY_LOCAL_DEV_API_KEY_SECRET" dotnet user-secrets set "EnvironmentSpecific:DatabaseConnectionString" "local_dev_db_connection_string_from_user_secrets"
- This creates a
secrets.jsonfile outside your project directory, specific to your user profile.
- Right-click the project in Visual Studio -> "Manage User Secrets". Or via CLI:
-
Understand
local.settings.json: This file is primarily for the Azure Functions Core Tools when running locally.IsEncrypted: Set tofalsefor local dev.Values: These key-value pairs are loaded as environment variables for your local function host. They will override settings fromappsettings.jsonandappsettings.Development.json.Host: Settings for the Functions host itself (e.g.,CORS,CORSCredentials).ConnectionStrings: Can be used for connection strings.
// local.settings.json { "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", // For localAzurite "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "AZURE_FUNCTIONS_ENVIRONMENT": "Development", // This sets IHostEnvironment.EnvironmentName // These will override appsettings.*.json and User Secrets if keys match // Because they become environment variables "MyApplication:ApiKey": "LOCAL_SETTINGS_API_KEY", // Example: if you prefer it here over User Secrets "EnvironmentSpecific:DatabaseConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MyLocalDevDb;Trusted_Connection=True;" // "MyKeyVaultUri": "https://my-dev-kv.vault.azure.net/" // For local Key Vault access if needed }, "ConnectionStrings": { // Alternative way to define connection strings // "MyDbConnection": "local_db_connection_string_from_local_settings" } }
IMPORTANT: Add
local.settings.jsonto your.gitignorefile if it contains any real secrets. It's common to commit alocal.settings.json.templateorlocal.settings.sample.jsonwith placeholder values. -
Accessing Configuration in your Function: Use Dependency Injection and the Options pattern.
// MyHttpFunction.cs using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Net; public class MyHttpFunction { private readonly ILogger<MyHttpFunction> _logger; private readonly MyApplicationSettings _appSettings; private readonly MyEnvironmentSpecificSettings _envSettings; private readonly IMyService _myService; // Inject IOptions<T> public MyHttpFunction( ILogger<MyHttpFunction> logger, IOptions<MyApplicationSettings> appSettings, IOptions<MyEnvironmentSpecificSettings> envSettings, IMyService myService) { _logger = logger; _appSettings = appSettings.Value; // Get the actual settings object _envSettings = envSettings.Value; _myService = myService; } [Function("MyHttpTrigger")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) { _logger.LogInformation("C# HTTP trigger function processed a request."); _logger.LogInformation("ServiceUrl from config: {ServiceUrl}", _appSettings.ServiceUrl); _logger.LogInformation("DefaultTimeoutSeconds from config: {Timeout}", _appSettings.DefaultTimeoutSeconds); _logger.LogInformation("ApiKey from config: {ApiKey}", _appSettings.ApiKey); // Will be null if not set anywhere _logger.LogInformation("DatabaseConnectionString from config: {DbConn}", _envSettings.DatabaseConnectionString); _logger.LogInformation("StorageAccountName from config: {StorageName}", _envSettings.StorageAccountName); var message = await _myService.DoSomethingAsync(); var response = req.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "text/plain; charset=utf-8"); await response.WriteStringAsync($"Welcome to Azure Functions! Message: {message}"); return response; } } public interface IMyService { Task<string> DoSomethingAsync(); } public class MyService : IMyService { private readonly string _apiKey; public MyService(IOptions<MyApplicationSettings> appSettings) { // Example of accessing specific value if needed directly in a service _apiKey = appSettings.Value.ApiKey ?? throw new ArgumentNullException("ApiKey is missing"); } public Task<string> DoSomethingAsync() => Task.FromResult($"Service used API key starting with: {_apiKey.Substring(0, Math.Min(5, _apiKey.Length))}"); }
-
Running Locally:
- Set
AZURE_FUNCTIONS_ENVIRONMENTinlocal.settings.jsonto "Development" (or "Test", "UAT" if you want to test otherappsettings.{env}.jsonfiles locally). - The Functions Host will read
local.settings.jsonand make itsValuesavailable as environment variables. - Your
Program.csconfiguration setup will then correctly loadappsettings.json,appsettings.Development.json, User Secrets, and then these environment variables (fromlocal.settings.json) will take final precedence for any overlapping keys.
- Set
Phase 2: Azure Deployment & Configuration
-
Azure Key Vault Setup:
- For each environment (dev, test, uat, prod), create a separate Azure Key Vault instance (e.g.,
myfunc-dev-kv,myfunc-test-kv,myfunc-prod-kv). - Store all secrets (API keys, connection strings, etc.) in these Key Vaults.
- Example Secret Name in Key Vault:
MyApplication--ApiKey(use double underscore--for section nesting, as it translates to:in configuration). - Another:
EnvironmentSpecific--DatabaseConnectionString
- Example Secret Name in Key Vault:
- For each environment (dev, test, uat, prod), create a separate Azure Key Vault instance (e.g.,
-
Azure Function App Configuration (App Settings): When you deploy your Function App to Azure, you configure it using "Application Settings" in the Azure portal (or via ARM/Bicep/Terraform).
AZURE_FUNCTIONS_ENVIRONMENT: Set this App Setting toDevelopment,Test,UAT, orProductionfor the respective Function App instance. This controls whichappsettings.{EnvironmentName}.jsonfile is loaded if present, and also theenv.IsDevelopment(),env.IsProduction()checks inProgram.cs.- Non-Sensitive Settings: Can be set directly as App Settings.
MyApplication:ServiceUrl=https://prod.api.example.comMyApplication:DefaultTimeoutSeconds=60
- Sensitive Settings (Key Vault References): This is the best practice.
- Grant your Function App's Managed Identity (System-Assigned or User-Assigned)
Get(and sometimesList) permissions on secrets in the corresponding Key Vault. - In the Function App's Application Settings, use Key Vault reference syntax:
- Name:
MyApplication:ApiKey - Value:
@Microsoft.KeyVault(SecretUri=https://myfunc-prod-kv.vault.azure.net/secrets/MyApplication--ApiKey/YOUR_SECRET_VERSION_GUID) - Or, for latest version:
@Microsoft.KeyVault(VaultName=myfunc-prod-kv;SecretName=MyApplication--ApiKey) - Similarly for
EnvironmentSpecific:DatabaseConnectionString.
- Name:
- These App Settings (sourced from Key Vault) become environment variables in the Function App's runtime, overriding any values from
appsettings.*.jsonfiles.
- Grant your Function App's Managed Identity (System-Assigned or User-Assigned)
-
.gitignore: Ensure these are in your.gitignore:# Local settings local.settings.json # User Secrets **/secrets.json # Binaries and build artifacts [Bb]in/ [Oo]bj/You should commit
appsettings.jsonand potentiallyappsettings.Development.json(if it contains no secrets),appsettings.Production.jsonetc., if they define structural or default non-sensitive configurations. Secrets always go into Key Vault for Azure environments.
Phase 3: Azure DevOps YAML Pipelines
Here's how to manage environment-specific configurations in Azure DevOps:
-
Variable Groups:
- Create Variable Groups for each environment (e.g.,
MyFunctionApp-Dev-Vars,MyFunctionApp-Test-Vars,MyFunctionApp-UAT-Vars,MyFunctionApp-Prod-Vars). - Option A (Recommended for Secrets): Link to Azure Key Vault.
- In your Variable Group, toggle "Link secrets from an Azure Key Vault as variables".
- Select your Azure Subscription and the appropriate Key Vault (e.g.,
myfunc-dev-kvfor the dev variable group). - Authorize the connection.
- Add the specific secrets you want to pull (e.g.,
MyApplication--ApiKey,EnvironmentSpecific--DatabaseConnectionString). These will become pipeline variables.
- Option B (For Non-Secrets): Define Variables Directly.
- You can define non-sensitive variables directly in the group (e.g.,
serviceUrl,timeout).
- You can define non-sensitive variables directly in the group (e.g.,
- Create Variable Groups for each environment (e.g.,
-
YAML Pipeline (
azure-pipelines.yml):trigger: - main # Or your main branch pool: vmImage: 'windows-latest' # Or 'ubuntu-latest' if your tools support it variables: - name: buildConfiguration # For dotnet build/publish value: 'Release' - name: dotnetVersion value: '9.0.x' # Specify your .NET version stages: - stage: Build jobs: - job: BuildJob steps: - task: UseDotNet@2 displayName: 'Use .NET SDK $(dotnetVersion)' inputs: packageType: 'sdk' version: '$(dotnetVersion)' - script: dotnet build --configuration $(buildConfiguration) displayName: 'Build solution' - task: DotNetCoreCLI@2 displayName: 'Publish Function App' inputs: command: 'publish' publishWebProjects: false # Important for Functions projects: '**/*.csproj' # Adjust if needed, point to your Function App csproj arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/App --runtime win-x64 --self-contained false' # Adjust runtime as needed zipAfterPublish: true - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: App' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/App' ArtifactName: 'App' publishLocation: 'Container' - stage: DeployDev displayName: 'Deploy to Development' dependsOn: Build condition: succeeded() # Or specific branch conditions variables: - group: MyFunctionApp-Dev-Vars # Link your DEV variable group jobs: - deployment: DeployFunctionAppDev environment: 'MyFunctionApp-Development' # Azure DevOps Environment strategy: runOnce: deploy: steps: - task: AzureFunctionApp@2 # Use version 2 or higher displayName: 'Deploy Azure Function App to Dev' inputs: azureSubscription: 'Your-Azure-Dev-Subscription-Service-Connection' appType: 'functionApp' # For Windows or functionAppLinux for Linux appName: 'my-dev-functionapp-name' # Your Function App name in Azure package: '$(Pipeline.Workspace)/App/**/*.zip' deploymentMethod: 'auto' # Or zipDeploy, runFromPackage appSettings: >- -AZURE_FUNCTIONS_ENVIRONMENT "Development" -MyApplication:ServiceUrl "$(devServiceUrl)" -MyApplication:ApiKey "@Microsoft.KeyVault(SecretUri=$(DevApiKeySecretUri))" -EnvironmentSpecific:DatabaseConnectionString "@Microsoft.KeyVault(SecretUri=$(DevDbConnSecretUri))" # Add other non-secret settings or Key Vault references here from your variable group # Example: -MyApplication:DefaultTimeoutSeconds "$(devDefaultTimeout)" # If using Key Vault references extensively, you might pre-configure them in ARM/Bicep # and only set AZURE_FUNCTIONS_ENVIRONMENT here. - stage: DeployTest displayName: 'Deploy to Test' dependsOn: Build # Or DeployDev if you want sequential deployment condition: succeeded() # And potentially other conditions (e.g., approval, branch) variables: - group: MyFunctionApp-Test-Vars jobs: - deployment: DeployFunctionAppTest environment: 'MyFunctionApp-Test' strategy: runOnce: deploy: steps: - task: AzureFunctionApp@2 displayName: 'Deploy Azure Function App to Test' inputs: azureSubscription: 'Your-Azure-Test-Subscription-Service-Connection' appType: 'functionApp' appName: 'my-test-functionapp-name' package: '$(Pipeline.Workspace)/App/**/*.zip' deploymentMethod: 'auto' appSettings: >- -AZURE_FUNCTIONS_ENVIRONMENT "Test" -MyApplication:ServiceUrl "$(testServiceUrl)" -MyApplication:ApiKey "@Microsoft.KeyVault(SecretUri=$(TestApiKeySecretUri))" -EnvironmentSpecific:DatabaseConnectionString "@Microsoft.KeyVault(SecretUri=$(TestDbConnSecretUri))" # ... Similar stages for UAT and Prod ... - stage: DeployProd displayName: 'Deploy to Production' dependsOn: Build # Or DeployUAT condition: succeeded() # Add manual approval for Prod variables: - group: MyFunctionApp-Prod-Vars jobs: - deployment: DeployFunctionAppProd environment: 'MyFunctionApp-Production' # This DevOps environment should have approvals configured strategy: runOnce: deploy: steps: - task: AzureFunctionApp@2 displayName: 'Deploy Azure Function App to Prod' inputs: azureSubscription: 'Your-Azure-Prod-Subscription-Service-Connection' appType: 'functionApp' appName: 'my-prod-functionapp-name' package: '$(Pipeline.Workspace)/App/**/*.zip' deploymentMethod: 'runFromPackage' # Recommended for prod appSettings: >- -AZURE_FUNCTIONS_ENVIRONMENT "Production" -MyApplication:ServiceUrl "$(prodServiceUrl)" -MyApplication:ApiKey "@Microsoft.KeyVault(SecretUri=$(ProdApiKeySecretUri))" -EnvironmentSpecific:DatabaseConnectionString "@Microsoft.KeyVault(SecretUri=$(ProdDbConnSecretUri))"
Explanation of
appSettingsin YAML:- The
appSettingsparameter in theAzureFunctionApp@2task allows you to set or override Application Settings in your Azure Function App during deployment. -SettingName "Value": For regular values.-SettingName "@Microsoft.KeyVault(SecretUri=$(PipelineVariableContainingSecretUri))": For Key Vault references. The pipeline variable (e.g.,DevApiKeySecretUri) would be defined in your Variable Group and linked to the Key Vault secret.- You can use pipeline variables (e.g.,
$(devServiceUrl)) that are defined in the linked Variable Group.
Azure DevOps Environments & Approvals:
- For UAT and Prod, configure "Environments" in Azure DevOps (Pipelines -> Environments).
- Add manual approval checks to these environments to ensure a human gate before deploying to critical stages.
- The
Summary of Best Practices:
- Consistent Configuration Model: Use
Microsoft.Extensions.Configurationacross local and Azure. - Environment Identification:
- Locally:
AZURE_FUNCTIONS_ENVIRONMENTinlocal.settings.json. - Azure:
AZURE_FUNCTIONS_ENVIRONMENTas an App Setting in the Function App.
- Locally:
- Configuration Files:
appsettings.jsonfor defaults.appsettings.{EnvironmentName}.jsonfor environment-specific non-sensitive overrides. Commit these.
- Local Development Secrets:
- User Secrets (preferred for sensitive items not directly related to Functions host).
local.settings.json(ensure it's in.gitignoreif it contains real secrets). Values here become environment variables locally.
- Azure Secrets: Azure Key Vault is non-negotiable. Use Key Vault references in Function App Settings.
- Accessing Settings: Use Dependency Injection with
IOptions<T>for strongly-typed configuration. - Azure DevOps:
- Use Variable Groups, linking to Key Vault for secrets for each environment.
- Use stage-specific deployments in your YAML pipeline.
- Set
AZURE_FUNCTIONS_ENVIRONMENTand other Key Vault references/app settings via theAzureFunctionApp@2task'sappSettingsparameter. - Alternatively, manage App Settings entirely via IaC (Bicep/ARM/Terraform) and only set
AZURE_FUNCTIONS_ENVIRONMENTif it's dynamic per stage. However, Key Vault references often still need to be set on the App resource. The pipeline'sappSettingsallows dynamic updates.
This comprehensive approach provides a robust, secure, and maintainable way to manage configurations for your .NET 9 Azure Functions across all environments.
Resources:
- ASP.NET Core Configuration: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/ (Principles apply to isolated worker)
- Azure Functions - local.settings.json: https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-local#local-settings-file
- Dependency injection in .NET Azure Functions (isolated worker): https://docs.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide#dependency-injection
- Key Vault References for App Service and Azure Functions: https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references
- Azure DevOps Variable Groups: https://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups
- AzureFunctionApp@2 Task: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-function-app
- User Secrets in .NET: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets