dotnet user-secrets

Keep development credentials out of the repository with dotnet user-secrets, and know exactly when and where the host loads them.

Signature
dotnet user-secrets init | set <key> <value> | list | remove <key> | clear [--project <path>] [--id <UserSecretsId>]
  • Added in .NET .NET Core 1.0 (bundled with the SDK since .NET Core 2.1)

Parameters

ParameterTypeDescription
$initoptionalverbAdds a <UserSecretsId> GUID to the project file.
$set <key> <value>optionalverbStores a value under a configuration key such as ConnectionStrings:Orders. Reads JSON from standard input when no key and value are given.
$listoptionalverbPrints every stored key and value in plain text.
$remove <key> / clearoptionalverbDeletes one key or every key for the project.
$--project, -poptionalpath= the project in the current folderProject whose UserSecretsId is used.
$--idoptionalstringUse this secrets id directly instead of reading it from a project.

Return value

process exit code0 on success. Non-zero when the project has no UserSecretsId (except init) or the project cannot be found.

Errors

Prints "Could not find the global property 'UserSecretsId'" when the project was never initialised. Nothing fails at run time: missing secrets just leave keys unset.

Changelog

  • PHP .NET Core 3.0dotnet user-secrets init writes the UserSecretsId into the project file.

dotnet user-secrets stores development configuration in a JSON file in your user profile, outside the repository, keyed by the project's <UserSecretsId>. It is a configuration source, not a vault: the file is plain text and anyone with access to your account can read it. Its only job is keeping local connection strings and API keys out of git.

Where the file is

OSPath
Windows%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json
Linux, macOS~/.microsoft/usersecrets/<id>/secrets.json

Keys are flat configuration paths (ConnectionStrings:Orders, Payments:ApiKey), so they bind like any other source. Two projects can share secrets by sharing a UserSecretsId, typically the API and its worker.

When they load

WebApplication.CreateBuilder and Host.CreateApplicationBuilder add user secrets only when the environment is Development and the application assembly carries a UserSecretsIdAttribute, which the SDK generates from the project property. They sit after appsettings.Development.json and before environment variables and the command line, so an environment variable still overrides a secret.

The common "my secret is not loading" causes, in order:

  1. The environment is not Development: --no-launch-profile, a test host set to another environment, or a worker run without DOTNET_ENVIRONMENT.
  2. The secret was set for a different project (a class library) whose id the app does not use. Pass --project pointing at the app.
  3. The key path differs: ConnectionStrings:orders works because keys are case-insensitive, but ConnectionStrings.Orders is a different key.
  4. A plain ConfigurationBuilder or Host.CreateEmptyApplicationBuilder, which adds nothing; call AddUserSecrets<Program>() yourself, before AddEnvironmentVariables and AddCommandLine.

The production failure

User secrets never ship. Code that works locally because a secret exists gets a null connection string in staging, and fails on the first query instead of at start-up. Bind the value to options with ValidateOnStart so a missing key stops the deployment, and supply production values through environment variables or a secret store. Never set ASPNETCORE_ENVIRONMENT=Development on a server to make secrets "work".

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Keep a local PostgreSQL password for the orders database out of appsettings.Development.json and out of git
  • Share one set of payment sandbox keys between Orders.Api and Orders.Worker by giving both the same UserSecretsId
  • Onboard a developer by piping a JSON file of sandbox keys into dotnet user-secrets set

Frequently asked questions

Are user secrets encrypted?

No. secrets.json is plain JSON in your profile folder. It keeps secrets out of the repository and nothing more; production secrets belong in environment variables or a secret store.

Why does my integration test not see user secrets?

Secrets load only in Development. If the test factory sets another environment, or the secret was set on a different project id, the key is missing. Prefer explicit test configuration over relying on a developer's secrets.

Do I commit the UserSecretsId?

Yes. It is a folder name, not a secret. Each developer's values stay in their own profile.

More examples

Setting values, including in bulk

Piping JSON into <code>set</code> with no key flattens and stores the whole file, which is a quick way to onboard a new developer from a shared, access-controlled source. <code>list</code> prints values in clear text, so do not run it on a shared screen.

Example · bash
cd src/Orders.Api
dotnet user-secrets init

dotnet user-secrets set "ConnectionStrings:Orders" "Host=localhost;Port=5432;Database=orders;Username=orders_dev;Password=<local-dev-password>"
dotnet user-secrets set "Payments:ApiKey" "<sandbox-key-from-provider-dashboard>"

# Bulk import from a JSON file kept outside the repository.
cat ~/dev-config/orders-api.secrets.json | dotnet user-secrets set

dotnet user-secrets list
dotnet user-secrets remove "Payments:ApiKey"

A worker on an empty builder that loads secrets explicitly

The empty builder is the case where <code>AddUserSecrets</code> is needed, and the order of the calls is the precedence: secrets after the JSON files and before environment variables and the command line, exactly like <code>CreateApplicationBuilder</code>. <code>ValidateOnStart</code> runs when <code>RunAsync</code> starts the host, so a missing secret stops the worker with a clear message instead of failing on the first payment.

Example · csharp
using Microsoft.Extensions.Options;

// CreateEmptyApplicationBuilder adds no configuration sources at all, so the
// worker lists them itself. CreateApplicationBuilder already does this; never
// call AddUserSecrets again there, or secrets end up after environment variables.
var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings
{
    Args = args,
    EnvironmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? Environments.Production,
    ContentRootPath = AppContext.BaseDirectory,
});

builder.Configuration
    .SetBasePath(builder.Environment.ContentRootPath)
    .AddJsonFile("appsettings.json", optional: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true);

// Order decides precedence: later sources win, so secrets go before env vars and args.
if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>(optional: true);
}

builder.Configuration.AddEnvironmentVariables().AddCommandLine(args);

builder.Services.AddOptions<PaymentsOptions>()
    .Bind(builder.Configuration.GetSection("Payments"))
    .Validate(o => !string.IsNullOrWhiteSpace(o.ApiKey), "Payments:ApiKey is required")
    .ValidateOnStart();

builder.Services.AddHostedService<PaymentsSettlementWorker>();

// RunAsync starts the host; ValidateOnStart throws here, before any worker runs.
await builder.Build().RunAsync();

public sealed class PaymentsOptions
{
    public string BaseUrl { get; set; } = "https://api.sandbox.payments.example";
    public string ApiKey { get; set; } = "";
}

public sealed class PaymentsSettlementWorker(IOptions<PaymentsOptions> options) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine($"Settling payments against {options.Value.BaseUrl}");
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }
}

public partial class Program;

What the project file holds

The id is only a folder name, so it belongs in source control and every developer gets their own <code>secrets.json</code> under it. Copy the same id into the worker project if both should read the same local secrets.

Example · xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <!-- Added by dotnet user-secrets init. Not a secret: safe to commit. -->
    <UserSecretsId>4f1d6c2a-7b0e-4d8f-9a51-3c2e8b6f0d17</UserSecretsId>
  </PropertyGroup>
</Project>

Discussion

  • Be the first to comment on this lesson.