Skip to main content

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:

SystemLocation
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:

ProjectWhenSecrets loading
Serverat runtimeASPNETCORE_ENVIRONMENT=Development only
Modelat design time (dotnet ef migrations add, dotnet ef database update, …)always
Batchat runtimeDOTNET_ENVIRONMENT=Development only
Batch environment

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
└── ...
Directory.Build.props
<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.

Generate a GUID (PowerShell)
[guid]::NewGuid()
Generate a GUID (Linux / macOS)
uuidgen
warning
No dotnet user-secrets init

Do 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 :.

From the Server folder
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:

Server/appsettings.json
{
"ConnectionStrings": {
"MainDb": "Server=db01;Database=App;User Id=app;Password=${DB_PASSWORD}"
}
}
In development: user secret
dotnet user-secrets set "DB_PASSWORD" "..."
In production: environment variable
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.

SyntaxResult
${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}" or Password='${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:

Server/appsettings.json
{
"Lagoon": {
"ExpandPlaceholders": false
}
}

Loading order​

OrderSourceServer and batch (Development)Design time (dotnet ef)
1appsettings.json✔✔ (from the Server project)
2appsettings.{Environment}.json✔✔ (appsettings.Development.json from the Server project)
3User secrets✔✔
4Environment variables✔✔
5Command-line arguments✔—
6Placeholder 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.

Model/Context/DesignTimeDbContextFactory.cs
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.