Introduction
When working with any .NET application, we normally require to set up some settings that will be different depending on the environment running.
The default, recommended and broadly used mechanism in this case is under appSetting.json files in the root of the starting project.
Having said that, there are multiple ways to properly manage, and even though it may seem like an easy scenario to manage, without proper care and organization it is easy to end up turning the settings files into a little mess drawer of configurations.
I will try to explain though, the one that I have been using the most, considering the following bullet points:
- Use the most simple mechanism possible
- Avoid settings keys redundancy as much as possible
- Secure management
With this aim, we are going to focus on the following scenario:
- AspNetCore WebAPI project >= 6.0
- Visual Studio 2022
- dotnet secrets
- Azure AppService
- Azure Key Vault
appSettings files
Well, imagine our project having a couple of environments in the cloud: one for test, and the Production one. With the aim of having specific configurations for local Development and both cloud environments, we would have something like this:
- appSettings.json
- appSettings.Development.json
- appSettings.Test.json
- appSettings.Production.json
But, then how can easily differenciate where to add the settings in every scenario? For example: What if I have a setting that should be the same for all environments except Production?
Given this, my current approach is the following:
- appSettings.json: General setttings, can apply to every environment.
- appSettings.Development.json: Specific configurations only used in local development. Overrides in case of conflict our appSettings.json.
- appSettings.Test.json && appSettings.Production.json Specific configurations for our cloud environments, overrides in case of conflict our appSettings.json.
Microsoft.Extensions.Configuration and WebApplicationBuilder
The default AspNetCore template installs all the required NuGet packages for a typical WebAPI throught the metapackage Microsoft.AspNetCore.App.
In our case, the WebApplicationBuilder will use these packages to build our configuration based on what we have configured between the appSettings files and the local user-secrets.
1. What to configure in global appSettings.json
The global appSettings.json is the first file that the WebApplicationBuilder will take into account. These are therefore those configurations that apply globally to the project, and you will see many approaches on how to manage it.
There are developers who prefer that this file be self-sufficient, containing a complete development environment configuration, but I prefer to write all configuration keys and to only provide a non-empty value when the same value is shared between all environments. With this strategy I try to achieve the following:
- Clarity on whether the setting is global or environment specific.
- Prevent accidental misconfiguration: I prefer that the construction of the settings be forced between both files (global + env) so that the environment only works if it really has its settings configured, and if it doesn't throw an exception. Imagine forgetting to put the proper connection string in prod, but having the environment working correctly pointing to test DB because the global file allows it.
In any case, what is never recommended is to use any PROD setting in the global file, since this could have dire consequences during the development and testing process. Therefore, the PROD settings can only be in their corresponding file.
So, given that, for instance let's add the connectionString to our database, and the settings for connecting through an external API protected by an API token.
{
"ConnectionStrings": {
"SqlServerConnection": ""
},
"ExternalServices": {
"ExternalApiSettings": {
"EndpointUrl": "",
"Token": ""
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
We could even not create the sections and just define it in each environment appSettings, but personally I like to be able to peek in the global file for all the necessary project's settings, knowing that those empty ones will be overwritten by the environment in question, or by the secrets manager.
2. What to configure in appSettings.{env}.json (test, production)
Now we know that after having loaded the global file, AspNetCore will load the appSettings values of the running environment. This is done through the environment variable ASPNETCORE_ENVIRONMENT.
Since the value of it is "Development" in local, it will be in charge of loading the appSettings.Development.json values, even overwriting the global ones, just as we want.
So given that, we can have multiple options. For instance, since we are locally running an SqlServer instance for our local development using Integrated Security, and this connection string is not exposing any kind of sensitive data, we can do the following for our scenario:
appSettings.Development.json
{
"ConnectionStrings": {
"SqlServerConnection": "Data Source=localhost;Initial Catalog=TestDatabase;Integrated Security=True;TrustServerCertificate=True"
},
"ExternalServices": {
"ExternalApiSettings": {
"EndpointUrl": "https://www.apisandbox.com/api"
}
}
}
In our example, we are not configuring anything particular in local environment for logging, so given this, we can just remove the entire section.
Additionally, we only specify the URL for the testing sandbox provided for our external API service, so now we only have to configure the Token so that our local environment will be fully functional.
appSettings.Test.json
{
"ExternalServices": {
"ExternalApiSettings": {
"EndpointUrl": "https://www.apisandbox.com/api"
}
}
}
appSettings.Production.json
{
"ExternalServices": {
"ExternalApiSettings": {
"EndpointUrl": "https://www.apiprod.com/api"
}
}
}
However, in our Azure environment files (Test, Production) we want to securely control our database connection, so we can avoid overwrite this value at this stage, and as on Development, only the External Service url can be placed here since it's not exposing any sensitive information and should not be part of the Azure Vault configuration later on.
3. Configure dotnet secrets
.NET by default provides us with a secure credential store outside the git repository, for this we have two very simple ways to configure it.
Option A: By using Visual Studio integrated view.
The simplest will just require us to right click on the startup project and then on "Manage user secrets".
Option B: By using dotnet cli
It's no more difficult to do from the dotnet cli, just running the following command in the root of the startup project will do exactly the same:
dotnet user-secrets init
I both cases, what we will be able to see is a new tag in our csproj file like this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<!-- [...] -->
<!-- Our autogenerated unique ID for storing sensitive configurations -->
<UserSecretsId>69f106e3-5df1-4e94-8ef6-3f964a3fdb98</UserSecretsId>
</PropertyGroup>
<!-- [...] -->
</Project>
Then we can again configure our settings both from the built-in tool in Visual Studio, or from the dotnet cli
Using Visual Studio
If done the previous step, just right clicking on the startup project and then on "Manage user secrets" it will show us a json editor where we can add the same configuration that we can see in the json later.
Using dotnet cli
Simply running the following command from the console will have the same effect:
dotnet user-secrets set "ExternalServices:ExternalApiSettings:Token" "OurSecretToken"
Note that we are specifying three levels of nesting.
This will generate the following JSON file stored in the following user's path:
%APPDATA%\Microsoft\UserSecrets\69f106e3-5df1-4e94-8ef6-3f964a3fdb98\secrets.json
{
"ExternalServices:ExternalApiSettings:Token": "OurSecretToken"
}
Note that the UserSecretsId is the one specified in the path, this will ensure that all developers pulling the code will have the same ID in their local secrets manager, easing the configuration to each team member.
As you also may have noticed, the dotnet secrets manager collapses the configurations, so that we can have an easy to manage key-value equivalent, however if we prefer a prettier JSON formed in the same way, we can manipulate it and it will equally work.
{
"ExternalServices": {
"ExternalApiSettings": {
"Token": "OurSecretToken"
}
}
}
Time to run the project again, and we can observe, regardless of the path followed and indicated above, that the settings are correctly merged between files and loaded, in a well, clean and secure way:
Road to Azure
Although in this article I am not going to explain how to create an Azure Vault or an AppService, we will go into the details that are required to properly link one to the other, and make sure that our settings are properly configured in each of the environments.
Since the official Microsoft documentation has good resources explaining these parts better than me, I leave here the links for those who require some information on how to perform these steps:
1. App Service
The first thing we will need is to provide our AppService with a managed identity in order to guarantee controlled access to our Azure Vault.
For this, Azure provides a very simple configuration tab where a unique identifier will be assigned for it.
By default, this option is inactive in an AppService, so it will be required to activate it,
Once configured, we can copy it for properly finding in the next step.
2. Azure Vaults
Already configured the managed Id for our AppService, we only have to give it permissions so that it can read the secrets configured inside.
It is important to remember that each instance must have its own associated Vault, otherwise we could not properly configure the access control either by application or by users who are in charge of maintaining such environment.
Giving permissions to our AppService
So, in our created Azure Vault:
Then we have to click on (+) to create.
For our current needs, we can configure it like this:
Once selected, we will be able to filter having the unique Id copied from the AppService Identity menu.
Note: It's important to remind that these configurations can also be done from Azure cli. More information here
Creating secrets in Vaults
Clicking on (+) Generate/Import we will be able to create the secrets that will be overwriting the ones configured in appSetting.json.
Please put special attention at how the configuration is boiled down using double hyphen notation to separate configuration section names.
Having done that, we have finished our Azure configuration between our AppService and Azure Vault for being able to properly replace our sensitive data in runtime once deployed on our Azure AppServices.
3. Azure Vault code integration
Back to our project again, the last step is to make sure that any values specified in a Vault can overwrite existing configuration specified in the appsettings files.
Required NuGets
- Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
- Azure.Identity
dotnet add package Azure.Identity
Code configuration
AppSettings
In the environment's appSettings.{env}.json (Test, Production)
For each environment, we have to add a new property in the json file:
[...]
"AzureKeyVaultUrl": "https://ourtestkeyvault.vault.azure.net/"
[...]
The Vault url can be obtained on the main screen of each Vault.
Program.cs
Through the WebApplicationBuilder, add the following code protected by a precompilation directive based on Debug mode:
#if !DEBUG
var keyVaulUrl = builder.Configuration["AzureKeyVaultUrl"];
if(keyVaulUrl == null)
throw new ArgumentNullException("AzureKeyVaultUrl must be configured for the given environment");
builder.Configuration.AddAzureKeyVault(new Uri(keyVaulUrl), new DefaultAzureCredential());
#endif
This protection will allow us to only use the dotnet-secrets in a local environment, and in any other scenario it will be doing it through the Vaults.
Conclussion
Proper management of settings will allow for an isolated development environment for each developer, as well as well-separated management in the different environments with control over them managed segregated by teams, individuals, roles... etc.
And most important of all, the process allows us to manage our configurations in a completely secure way, both locally and in Azure.
Resources
The entire code can be found here
Useful links
If you liked it, please give me a ⭐ and follow me 😉
Top comments (5)
Very interesting and useful article @xelit3
Thanks for sharing!
Thanks for your kind words @joseluiseiguren ! Great to see that it's helpful for the community folks! 😄
Really interesting. In your opinion there is an advantage of setting up the configuration on Azure Cloud or it is just to show how to do it?
Hi @poteking!
Thanks for reading it and raising the question! 😃
Initially, is simply the most common scenario with AspNetCore since Azure and dotnet both fit perfectly fine.
However it will depend on the Cloud provider that you have, for instance you have Secrets manager in AWS with similar purpose. Therefore, over the paper same principles explained here are valid independently of cloud provider used.
Have I clarified the doubt?
Absolutly, loud and clear. Thank you so much for your answer 🙂.