Chrysalit.Extensions.UserSecrets 1.0.0

The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org. Prefix Reserved

Requires NuGet 2.8 or higher.

dotnet add package Chrysalit.Extensions.UserSecrets --version 1.0.0
NuGet\Install-Package Chrysalit.Extensions.UserSecrets -Version 1.0.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Chrysalit.Extensions.UserSecrets" Version="1.0.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Chrysalit.Extensions.UserSecrets --version 1.0.0
#r "nuget: Chrysalit.Extensions.UserSecrets, 1.0.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Chrysalit.Extensions.UserSecrets as a Cake Addin
#addin nuget:?package=Chrysalit.Extensions.UserSecrets&version=1.0.0

// Install Chrysalit.Extensions.UserSecrets as a Cake Tool
#tool nuget:?package=Chrysalit.Extensions.UserSecrets&version=1.0.0

Chrysalit.Extensions.UserSecrets

This package provide extensions methods for the well-known Microsoft.Extensions.Configuration.UserSecrets package available from the dotnet runtime.

Globally the source code is very similar, but the sole purpose is to provide capability to manage "environmental" secrets (i.e. secrets.{env}.json) the same way it is already done with appsettings.json and the associated environemental file appsettings.Development.json in the various samples provided.

This package use the IHostEnvironment configuration defined on the IHost. See examples for a quick overview.

Original Problem

It is common that applications are deployed on several environments. And it is not uncommon willing to debug an application on several environments, whatever during an active development phase, to access production-grade data quality, debug a specific issue ...

While Microsoft.Extensions.Configuration.UserSecrets is adviced for the developper experience, switching secrets from an environment to another is not straightforward.

As developers, committing "secrets" in sources control system (SCM/Git) is not recommended, and in the same time having the capability to quickly launch an application for several environments with different connections strings, API passwords and so on ... would be helpful.

By default Microsoft.Extensions.Configuration.UserSecrets use only one "secrets.json" file, even if it stored in a dedicated folder with the name of Guid of the UserSecretsId property.

Solution

The changes provided in this package are so simple that it could be proposed as a PR on the official Dotnet Runtime repository.

Installation

Nuget

The package is available on Nuget.

dotnet add package Chrysalit.Extensions.UserSecrets

Examples

Examples provide various stategy to define an "environment" that will be used by the new extensions methods introduced in this package to load the secret files in order:

  • secrets.json
  • secrets.dev.json

Using UserSecrets

The recommended approach is to use the MSBuild property which is automatically generated and defined in your project .csproj file when installing Microsoft.Extensions.Configuration.UserSecrets Nuget package.

MSBuild Property

Define the UserSecretsId with a new generated Guid value in the .csproj file:

  <PropertyGroup>
    <UserSecretsId>d56b659d-5c1e-4b9d-a90f-517cdf0ed624</UserSecretsId>
  </PropertyGroup>

Assembly

Define the UserSecretsId as attribute of your namespace:

[assembly: UserSecretsId("d56b659d-5c1e-4b9d-a90f-517cdf0ed624")]
namespace Sample {
  // omitted for brievety
}

Nuget Packages and using statements

The examples below uses the following Nuget packages:

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.UserSecrets
  • Microsoft.Extensions.Hosting
  • Chrysalit.Extensions.UserSecrets

All examples below requires the following using statements:

using Chrysalit.Extensions.UserSecrets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

Host.CreateDefaultBuilder()

For this example the dev environment is explicitely declared. Order doesn't matter, UseEnvironment may be declared after ConfigureAppConfiguration.

var host = Host.CreateDefaultBuilder(args)
    .UseEnvironment("dev")
    .ConfigureAppConfiguration((context, config) =>
    {
        config.AddUserSecrets<Program>(context);
    })
    .Build();

context is a HostBuildContext object. By default the CreateDefaultBuilder will load the default secrets.json file. But our new config.AddUserSecrets<Program>(context) will load the secrets.dev.json file and overwrite configuration values.

AddInMemoryCollection()

Note that ConfigureHostConfiguration delegate is called before ConfigureAppConfiguration delegate. Methods may be declared in any order but this one is better for clarity.

var host = new HostBuilder()
    .ConfigureHostConfiguration(config =>
    {
        config.AddInMemoryCollection(
        [
            new KeyValuePair<string, string?>(HostDefaults.EnvironmentKey, "dev")
        ]);
    })
    .ConfigureAppConfiguration((context, config) =>
    {
        config.AddUserSecrets<Program>(context);
    })
    .Build();

AddEnvironmentVariables()

Configure your application to use the DOTNET_ENVIRONMENT environment variable.

var host = new HostBuilder()
    .ConfigureHostConfiguration(config =>
    {
        config.AddEnvironmentVariables("DOTNET_");
    })
    .ConfigureAppConfiguration((context, config) =>
    {
        config.AddUserSecrets<Program>(context);
    })
    .Build();

Command

Configure your application to use the --environment dev

var host = new HostBuilder()
    .ConfigureHostConfiguration(config =>
    {
        config.AddCommandLine(args);
    })
    .ConfigureAppConfiguration((context, config) =>
    {
        config.AddUserSecrets<Program>(context);
    })
    .Build();

From Configuration (appsettings.json)

Content of the "appsettings.json" file:

{
  "environment": "dev"
}

Code

var host = new HostBuilder()
    .ConfigureHostConfiguration(config =>
    {
        config.AddJsonFile("appsettings.json")
    })
    .ConfigureAppConfiguration((context, config) =>
    {
        config.AddUserSecrets<Program>(context);
    })
    .Build();

Example lauchprofile.json

This launchSettings.json provides debug configurations for the examples above.

{
  "profiles": {
    "Debug Default": {
      "commandName": "Project"
    },
    "Debug Environment Variable": {
      "commandName": "Project",
      "environmentVariables": {
        "DOTNET_ENVIRONMENT": "dev"
      }
    },
    "Debug CommandLine Args": {
      "commandName": "Project"
      "commandLineArgs": "--environment dev",
    }
  }
}

Managing Environment UserSecrets

While this package provide the capability to load "environment" secrets at runtime, management of these various secrets files is not as straighforward it should be.

The following sections provide some hints.

Integration with SlowCheetah VS Extension

SlowCheetah is a VS Extension that allow powerful transformation on configuration files.

When combining SlowCheetah with the From Configuration, it becomes easier to define the environment key in appsettings.{environment}.json files, and uses this package to load the associated secrets.{environment}.json file.

appsettings.json is transformed and available in the output when building the project, while secrets.{environment}.json is only loaded at runtime depending of the value of the enviroment configuration key.

The only issue is that it is not straitforward to manage environments secrets directly from the Solution Explorer in Visual Studio.

While it is possible to configure secrets.json files to be available in the project, managing secrets.json using SlowCheetah can add transormations automatically but there's somme caveats:

  • transformations files created are added with a relative path to the project .csproj file.
  • secrets.json is output to the build directory.

dotnet user-secrets limits

Interestingly, the dotnet tool user-secrets has an option to specify a MSBuild configuration but this option is useless. The secrets.json file is always used.

dotnet user-secrets --configuration Release --verbose set "apikey" "123456"

The --verbose displays the path of the secrets.json file being modified.

MSBuild Configuration

One of the best method may be to diplay the secrets.json and associated secrets.{environment}.json in the Solution Explorer. This does allow:

  • Display secrets files associated with the build configuration
  • secrets.json is not output to the build directory.

Adding a new environment secret file is not well integrated (by example when adding a custom build configuration), but editing the project .csproj file seems mandatory in any case.

  <PropertyGroup>
    <SecretPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(AppData)\Microsoft\UserSecrets\$(UserSecretsId)\</SecretPath>
    <SecretPath Condition="!$([MSBuild]::IsOSPlatform('Windows'))">$(Home)/.microsoft/usersecrets/$(UserSecretsId)/</SecretPath>
  </PropertyGroup>
	
  <ItemGroup>
	<None Include="$(SecretPath)secrets.json"/>
	<None Include="$(SecretPath)secrets.Release.json">
	  <DependentUpon>secrets.json</DependentUpon>
	</None>
	<None Include="$(SecretPath)secrets.Debug.json">
      <DependentUpon>secrets.json</DependentUpon>
	</None>
  </ItemGroup>

The result is displayed below:

Secrets in Solution Explorer

More information about this MSBuild configuration is provided for information.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 95 3/11/2024