Secrets and configuration placeholders
The appsettings.json and appsettings.Development.json files are versioned: they must not contain passwords, API keys, or connection strings to a shared database. In development, these values go in the .NET user secrets store (dotnet user-secrets); in production, in environment variables or a secrets vault. Placeholders ${Key} allow you to keep a versioned value (a connection string, for example) and only store the secret part elsewhere.
The user secrets store is a JSON file stored in the developer's profile, outside the project folder:
| System | Location |
|---|---|
| Windows | %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json |
| Linux / macOS | ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json |
A Lagoon application reads these secrets in three places. All three must therefore use the same UserSecretsId:
| Project | When | Secrets loading |
|---|---|---|
Server | at runtime | ASPNETCORE_ENVIRONMENT=Development only |
Model | at design time (dotnet ef migrations add, dotnet ef database update, …) | always |
Batch | at runtime | DOTNET_ENVIRONMENT=Development only |
The batch reads its environment in DOTNET_ENVIRONMENT, not in ASPNETCORE_ENVIRONMENT. Set
DOTNET_ENVIRONMENT=Development in the Batch/Properties/launchSettings.json profiles, otherwise the batch does not load
the secrets.
Declaring UserSecretsId in Directory.Build.props
dotnet user-secrets init writes a <UserSecretsId> in one .csproj only. The other projects in the application do not have one, or receive a different one if they are initialized separately. Instead, declare the GUID once, in a Directory.Build.props file placed in the application's folder, above the Server, Model, Batch… folders. MSBuild automatically applies it to all projects below it.
APP/
├── Directory.Build.props ← Common UserSecretsId
├── Server/APP.Server.csproj
├── Model/APP.Model.csproj
├── Batch/APP.Batch.csproj
└── ...
<Project>
<!-- A single user secrets store for the whole application: the "Server" project reads it at runtime
(Development environment) and the "Model" project at design time ("dotnet ef"). -->
<PropertyGroup>
<UserSecretsId>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</UserSecretsId>
</PropertyGroup>
</Project>
Replace xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx with a GUID specific to your application. Two applications with the same identifier would share their secrets.
[guid]::NewGuid()
uuidgen
dotnet user-secrets initDo not run dotnet user-secrets init and do not let Visual Studio (Manage user secrets) add a <UserSecretsId> in a .csproj. The .csproj is read after Directory.Build.props: its value would replace the common GUID for that project only, and the server and dotnet ef would read two different stores. If a <UserSecretsId> is already present in a .csproj, delete it.
Managing secrets
Run the commands from the folder of any project in the application, or pass --project. They all act on the same store, since the UserSecretsId is common. JSON sections are written with :.
dotnet user-secrets set "ConnectionStrings:<Name>" "Server=localhost;Database=App;User Id=app;Password=..."
dotnet user-secrets list
dotnet user-secrets remove "ConnectionStrings:<Name>"
dotnet user-secrets clear
A secrets value replaces the value with the same key in appsettings*.json. So leave a non-secret value in appsettings.json, or do not include the key at all, and place the real value in the secrets. To store only the password of a connection string separately, use a placeholder (next section).
Placeholders ${Key} in configuration
A configuration value can contain placeholders ${Key}. Each is replaced, when read, by the value of the configuration key Key, from any source: appsettings*.json, user secrets, environment variable, secrets vault connected as a configuration source… The connection string remains versioned, and only the password is stored elsewhere:
{
"ConnectionStrings": {
"MainDb": "Server=db01;Database=App;User Id=app;Password=${DB_PASSWORD}"
}
}
dotnet user-secrets set "DB_PASSWORD" "..."
export DB_PASSWORD="..."
The application reads the complete connection string with Configuration.GetConnectionString("MainDb"), without a specific call: the replacement is automatic, including for related options (GetSection(...).Bind(...), IOptions<T>) and for libraries that read the configuration themselves.
| Syntax | Result |
|---|---|
${DB_PASSWORD} | value of the DB_PASSWORD key (user secret or environment variable DB_PASSWORD) |
${Database:Password} | value of the Database:Password key (environment variable Database__Password) |
${Database:Port ?? 1433} | value of the key, or 1433 if it does not exist (${Key ??} gives an empty string) |
$${ | the literal text ${ |
A key name cannot contain ?. A default value is literal text: it cannot contain } or placeholder. A ${ without a closing } is left as is.
Rules:
- Missing key: without
??, reading the value raises an exception that names the read key and the missing key, never displaying a value. A key present but empty is replaced by an empty string. - No cascading replacement: if the inserted value itself contains
${…}, it is inserted as is. - Escaping in
ConnectionStrings: an inserted value that contains;,',"or edge spaces is surrounded by quotes (Password="a;b"), internal"being doubled. If the value is already delimited in the string (Password="${DB_PASSWORD}"orPassword='${DB_PASSWORD}'), only the character that delimits it ("or') is doubled in the inserted value. A password containing a;therefore does not break the connection string. The ODBC syntax{…}is not handled. - Scope: server, batch and design time (
dotnet ef). The Blazor client does not replace placeholders: its configuration (wwwroot/appsettings*.json) is public and must contain no secrets.
To disable replacement, for example if values contain ${ for another reason:
{
"Lagoon": {
"ExpandPlaceholders": false
}
}
Loading order
| Order | Source | Server and batch (Development) | Design time (dotnet ef) |
|---|---|---|---|
| 1 | appsettings.json | ✔ | ✔ (from the Server project) |
| 2 | appsettings.{Environment}.json | ✔ | ✔ (appsettings.Development.json from the Server project) |
| 3 | User secrets | ✔ | ✔ |
| 4 | Environment variables | ✔ | ✔ |
| 5 | Command-line arguments | ✔ | — |
| 6 | Placeholder replacement ${…} | ✔ | ✔ |
Each source replaces values from previous sources. An environment variable ConnectionStrings__<Name> (with __ instead of :) therefore takes precedence over secrets, at runtime as well as at design time, which is convenient for a CI pipeline. Placeholders are replaced last, with the final values from all sources.
Design time: DesignTimeApplicationDbContextFactory
The dotnet ef commands create the context with the DesignTimeDbContextFactory factory of the Model project, which derives from DesignTimeApplicationDbContextFactory<T> (see
EF Core migrations and seeding). The base class chooses the user secrets store based on the UserSecretsId compiled in the assembly that declares the context, that is, the Model project assembly, not the Server project one. The Directory.Build.props of the application gives Model the same UserSecretsId as Server: dotnet ef therefore reads the same secrets as the server. If the identifier were declared only in the server's .csproj, Model would have none, and secrets would be ignored at design time without an error message.
To add or remove configuration sources at design time, override OnConfigureConfiguration. It is called after the loading of appsettings*.json files.
protected override void OnConfigureConfiguration(IConfigurationBuilder builder)
{
// Keep the user secrets and the environment variables.
base.OnConfigureConfiguration(builder);
// Local file, not versioned, read last so that it overrides the other sources.
// Relative paths are resolved from the "Server" project folder, where the appsettings files live.
builder.AddJsonFile("appsettings.Local.json", true);
}
The placeholders ${Key} are replaced after all sources, including those added in OnConfigureConfiguration.
Production
User secrets are a development tool: they are not encrypted, and the server and batch only read them in the Development environment. In production, provide sensitive values through environment variables, a secrets vault, or the configuration file of the deployment server.