C# – How to programmatically update the User Secrets file

User Secrets are stored in secrets.json. This file is specific to your application. Once you know the path of secrets.json, you can load and update it.

Here’s an example of how to update secrets.json programmatically:

using Microsoft.Extensions.Configuration.UserSecrets;

//1. Find secrets.json
var secretsId = Assembly.GetExecutingAssembly().GetCustomAttribute<UserSecretsIdAttribute>().UserSecretsId;
var secretsPath = PathHelper.GetSecretsPathFromSecretsId(secretsId);

//2. Load and modify
var secretsJson = File.ReadAllText(secretsPath);
dynamic secrets = JsonConvert.DeserializeObject<ExpandoObject>(secretsJson, new ExpandoObjectConverter());
secrets.Password = "bye";

//3. Overwrite the file with changes
var updatedSecretsJson = JsonConvert.SerializeObject(secrets, Formatting.Indented);
File.WriteAllText(secretsPath, updatedSecretsJson);
Code language: C# (cs)

Note: 1) For brevity, this isn’t showing all using statements. 2) This is using Newtonsoft because it’s better than System.Text.Json at deserializing JSON to dynamic objects.

One real world scenario where you’d want to do this is the following: if you’re using User Secrets and updating appsettings.json programmatically, you’ll notice that updating a property in appsettings.json is pointless if that property is being overridden in secrets.json (which I’ll explain down below). To actually change the property’s value, you can update it in secrets.json programmatically.

In this article, I’ll go into more details and explain how this works.

How to find secrets.json

The secrets.json file is stored in the following location (on Windows):

%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.jsonCode language: plaintext (plaintext)

The <user_secrets_id> part of the path is specific to your project. When you add the user secrets file, a unique identifier is generated and added as a property to the .csproj file:

<PropertyGroup>
	<OutputType>Exe</OutputType>
	<TargetFramework>netcoreapp3.1</TargetFramework>
	<UserSecretsId>fccaadfc-3ce2-4efe-8ca6-a4144e965323</UserSecretsId>
</PropertyGroup>
Code language: HTML, XML (xml)

When you do a build, this UserSecretsId property is included as assembly metadata. This means you can get the value with reflection:

using System.Reflection;

var secretsId = Assembly.GetExecutingAssembly().GetCustomAttribute<UserSecretsIdAttribute>().UserSecretsId;
Code language: C# (cs)

Once you have this secret id, you can get the full path. I recommend using the path helper to resolve the path:

Microsoft.Extensions.Configuration.UserSecrets.PathHelper.GetSecretsPathFromSecretsId(secretsId)
Code language: C# (cs)

Loading secrets.json and modifying values

Once you have the path, you can load secrets.json and deserialize it to a dynamic object. This allows you to modify values. Once you’re done modifying, you can serialize the dynamic object and overwrite secrets.json to save your changes:

using System.Dynamic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

var secretsJson = File.ReadAllText(secretsPath);
dynamic secrets = JsonConvert.DeserializeObject<ExpandoObject>(secretsJson, new ExpandoObjectConverter());
secrets.Password = "bye";

var updatedSecretsJson = JsonConvert.SerializeObject(secrets, Formatting.Indented);
File.WriteAllText(secretsPath, updatedSecretsJson);
Code language: C# (cs)

Tip: Alternatively, you can use JsonNode to read/modify JSON.

Make sure to write the JSON as indented because secrets.json is a config file and meant to be human-readable.

Why deserialize to a dynamic object?

The settings in secrets.json are a subset of settings from appsettings.json. If you deserialize secrets.json to a class that represents all settings, you’ll end up overriding all settings.

Here’s an example to illustrate this problem. Let’s say you have the following Config class:

public class Config
{
	public string Password { get; set; }
	public string Url { get; set; }
	public int Timeout { get; set; }
}
Code language: C# (cs)

And appsettings.json looks like this:

{
  "Timeout": 5000,
  "Url": "https://localhost:12345",
  "Password": ""
}
Code language: JSON / JSON with Comments (json)

The Password property is overridden in secrets.json:

{
  "Password": "hi" 
}
Code language: JSON / JSON with Comments (json)

Now let’s say you are deserializing secrets.json using the Config class:

//Load 
var secretsJson = File.ReadAllText(secretsPath);
Config secrets = JsonConvert.DeserializeObject<Config>(secretsJson);
secrets.Password = "bye";

//Save
var updatedSecretsJson = JsonConvert.SerializeObject(secrets, Formatting.Indented);
File.WriteAllText(secretsPath, updatedSecretsJson);
Code language: C# (cs)

Take a look at secrets.json:

{
  "Password": "bye",
  "Url": null,
  "Timeout": 0
}
Code language: JSON / JSON with Comments (json)

Notice that the Url and Timeout properties are set to default values (null / 0).

Why is this a problem? Because when you use ConfigurationBuilder to load in appsettings.json with User Secrets, it’ll override all settings that are defined in secrets.json:

var config = new ConfigurationBuilder()
	.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
	.AddJsonFile("appsettings.json")
	.AddUserSecrets<Program>()
	.Build()
	.Get<Config>();

Console.WriteLine($"Url={config.Url} Timeout={config.Timeout}");
Code language: C# (cs)

This outputs the following:

Url= Timeout=0Code language: plaintext (plaintext)

Even though the Url and Timeout properties have values in appsettings.json, they are overridden by the values in secrets.json. This shows why you need to be careful about what you deserialize secrets.json into. Deserializing into a dynamic object is the simplest option for avoiding this problem.

Note: If you don’t want to use a dynamic object, you can deserialize to a class or use JsonNode (as mentioned up above).

Leave a Comment