When you are developing a new project and need to store settings for it, the first thing that comes to mind is to use the Appsettings.json file. With this file, you can store all settings in a single file and restore them easily.
For example, let's create a console project that has three settings: Id, Name and Version. In a command line prompt, type:
dotnet new console -o SecretStorage
cd SecretStorage
code .
This will open VS Code in the folder for the new project. Add a new file named appSettings.json and add this code in it:
{
"AppData": {
"Id": "IdOfApp",
"Name": "NameOfApp",
"Version": "1.0.0.0"
}
}
We will add a AppData class in the project:
public class AppData
{
public string Id { get; init; }
public string Name { get; init; }
public string Version { get; init; }
public override string ToString()
{
return $"Id: {Id}, Name: {Name}, Version: {Version}";
}
}
To read the settings into the class, we could use JsonDeserializer from System.Text.Json:
using System.Text.Json;
var settingsText = File.ReadAllText("appsettings.json");
var settings = JsonSerializer.Deserialize<Settings>(settingsText);
Console.WriteLine(settings);
You need to define a class Settings to read the data:
public class Settings
{
public AppData AppData { get; set; }
}
That's fine, but let's say you have different settings for development and production, you should have a copy of the appsettings.json with the modified values and some code like this one:
using System.Diagnostics;
using System.Text.Json;
string settingsText;
if (Debugger.IsAttached)
{
settingsText = File.ReadAllText("appsettings.development.json");
}
else
{
settingsText = File.ReadAllText("appsettings.json");
}
var settings = JsonSerializer.Deserialize<Settings>(settingsText);
Console.WriteLine(settings);
Things start to become complicated. If there was a way to simplify this... In fact, yes there is. .NET provides us with the ConfigurationBuilder class. With it, you can read and merge several files to get the configuration. The following code will merge the appsettings.json and appsettings.development.json into a single class. In production, all you have to do is to remove the appsettings.development.json from the package and only the production file will be used.
To use the ConfigurationBuilder class you must add the NuGet packages Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Binder with
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Binder
You will also have to add the Microsoft.Extensions.Configuration.Json package to use the AddJsonFile extension method.
One other thing that you will have to do is to tell msbuild to copy the settings file to the output directory. This is done by changing the csproj file, adding this clause
<ItemGroup>
<Content Include="appsettings*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
Once you do that, this code will read the config files, merge them and print the settings:
IConfigurationRoot config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false)
.AddJsonFile("appsettings.development.json", true)
.Build();
var appdata = config.GetSection(nameof(AppData)).Get<AppData>();
Console.WriteLine(appdata);
The second parameter in the AddJsonFile method tells that the appsettings.development.json file is optional and, if it's not there, it wont be read. One other advantage is that I don't need to duplicate all settings. You just need to add the overridden settings in the development file and the other ones will still be available.
Now, one more problem: let's say we are using an API that requires a client Id and a client Secret. These values are very sensitive and they cannot be distributed. If you are using a public code repository, like GitHub, you cannot add something like this to appsettings.json and push your changes:
{
"AppData": {
"Id": "IdOfApp",
"Name": "NameOfApp",
"Version": "1.0.0.0"
},
"ApiData": {
"ClientId": "ClientIdOfApp",
"ClientSecret": "ClientSecretOfApp"
}
}
That would be a real disaster, because your API codes would be open and you would end up with a massive bill at the end of the month. You could add these keys to appsettings.development.json and add it to the ignored files, so it wouldn't be uploaded, but there is no guarantee that this won't happen. Somebody could upload the file and things would be messy again.
The solution, in this case, would be to use the Secret Manager Tool. This tool allows you to store secrets in development mode, in a way that they cannot be shared to other users. This tool doesn't encrypt any data and must only be used for development purposes. If you want to store the secrets in a safe encrypted way, you should use something like the Azure Key Vault.
To use it, you should initialize the storage with
dotnet user-secrets init
This will initialize the storage and generate a Guid for it and add it to the csproj file:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>fc572277-3ded-4467-9c46-534a075f905b</UserSecretsId>
</PropertyGroup>
Then, you need to add the package Microsoft.Extensions.Configuration.UserSecrets:
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
We can now start utilizing the user secrets, by adding the new configuration type:
IConfigurationRoot config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false)
.AddJsonFile("appsettings.development.json", true)
.AddUserSecrets<Settings>()
.Build();
Then, we can add the secret data:
dotnet user-secrets set "ApiData:ClientId" "ClientIdOfApp"
dotnet user-secrets set "ApiData:ClientSecret" "ClientSecretOfApp"
As you can see, the data is flattened in order to be added to the user secrets. You can take a look at it by opening an Explorer window and going to %APPDATA%\Microsoft\UserSecrets\{guid}\secrets.json
:
{
"ApiData:ClientId": "ClientIdOfApp",
"ApiData:ClientSecret": "ClientSecretOfApp"
}
As you can see, there isn't any secret here, it's just a way to store data with no possibility to share it in an open repository.
You can get the values stored with
dotnet user-secrets list
To remove some key from the store, you can use something like
dotnet user-secrets remove ClientId
And to clear all data, you can use
dotnet user-secrets clear
If you have some array data to store, you will have to flatten in the same way, using the index of the element as a part of the name. For example, if you have something like
public class Settings
{
public AppData AppData { get; set; }
public ApiData ApiData { get; set; }
public string[] AllowedHosts { get; set; }
}
You can store the AllowedHosts data with
dotnet user-secrets set "AllowedHosts:0" "microsoft.com"
dotnet user-secrets set "AllowedHosts:1" "google.com"
dotnet user-secrets set "AllowedHosts:2" "amazon.com"
And you can read the settings with some code like this:
IConfigurationRoot config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false)
.AddJsonFile("appsettings.development.json", true)
.AddUserSecrets<Settings>()
.Build();
var settings = config.Get<Settings>();
foreach (var item in settings.AllowedHosts)
{
Console.WriteLine(item);
}
Console.WriteLine(settings.AppData);
Console.WriteLine(settings.ApiData);
As you can see, if you need something to keep your development data safe from uploading to a public repository, you can use the user secrets in the same way you would do by using a json file. This simplifies a lot the storage of config files and allows every developer to have their own settings.
The full source code for this project is at https://github.com/bsonnino/SecretStorage