/ConfigurationPlaceholders

Adds support for placeholders to the .NET configuration stack.

Primary LanguageC#MIT LicenseMIT

Icon

ConfigurationPlaceholders

Latest Release Latest Pre-Release Downloads License Build

Adds support for placeholders in any configuration source (e.g. appsettings.json).

Getting started

Install the ConfigurationPlaceholders package from NuGet:

Package manager:

Install-Package ConfigurationPlaceholders

Or via the .NET CLI

dotnet add package ConfigurationPlaceholders

You can add ConfigurationPlaceholders to your project with the AddConfigurationPlaceholders extension method.

 var builder = WebApplication.CreateBuilder( args ); 
 builder 
     .AddConfigurationPlaceholders( new InMemoryPlaceholderResolver( new Dictionary<String, String?> 
     { 
         { "FQDN", fullDomainName } 
     } ) ); 

This will replace the placeholder ${FQDN} with the fully qualified name of the machine running the application.
Placeholder in the appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "CertificateSubject": "${FQDN}"
}

You can specify any number of placeholder resolvers IPlaceholderResolver in the call to AddConfigurationPlaceholders.
Later added placeholder resolvers IPlaceholderResolver will override values from before added placeholder resolvers IPlaceholderResolver.

Placeholder verification

By default ConfigurationPlaceholders will check if values are provided for all placeholders. If there are any missing placeholder values a ConfigurationPlaceholderMissingException will be thrown.
You can change this behavior by passing another MissingPlaceholderValueStrategy to AddConfigurationPlaceholders.

The following strategies are available:

VerifyAllAtStartup (default)

Will check if values are provided for all placeholders. If there are any missing placeholder values a ConfigurationPlaceholderMissingException will be thrown.

Throw

You can think of this one as a lazy execution version of VerifyAllAtStartup.   A ConfigurationPlaceholderMissingException will be thrown when a configuration entry with a missing placeholder value is being accessed. If the value never gets accessed, no exception will be thrown.

UseEmptyValue

Placeholders for which no value is provided will be replaced with an empty string.
"Hello, ${MissingValue}" will result in "Hello, "

IgnorePlaceholder

Placeholders for which no value is provided will not be replaced. The resulting value will still contain the placeholder. "Hello, ${MissingValue}" will result in "Hello, ${MissingValue}"

Examples

You can find some examples using ConfigurationPlaceholders here

Available placeholder resolvers IPlaceholderResolver

InMemoryPlaceholderResolver

Resolves placeholder values from an in-memory lookup. Works similar like the AddInMemoryCollection configuration source.

new InMemoryPlaceholderResolver( new Dictionary<String, String?>
{
    { "ApplicationName", Assembly.GetExecutingAssembly().GetName().Name },
    { "ApplicationVersion", Assembly.GetExecutingAssembly().GetName().Version!.ToString() }
} )

CallbackPlaceholderResolver

Resolves placeholder values by invoking user provided value factories.

new CallbackPlaceholderResolver( new Dictionary<String, Func<String?>>
{
    { "Time", () => DateTime.Now.ToString( "HH:mm:ss.fff" ) }
} )

ConfigurationPlaceholderResolver

Searches for values in all configuration sources matching the placeholder key.

new ConfigurationPlaceholderResolver()

In this example LocalDb is build based on other values in appsettings.json:

{
  "Lookup": {
    "DataDir": "X:/Temp/",
    "DbDir": "${Lookup:DataDir}db/"
  },
  "LocalDb": "${Lookup:DbDir}store.db"
}

EnvironmentVariableResolver

Resolves placeholder values by searching for environment variables matching the placeholder key. The search is performed in this priority order:

  1. EnvironmentVariableTarget.Process
  2. EnvironmentVariableTarget.User
  3. EnvironmentVariableTarget.Machine
new EnvironmentVariableResolver()

Custom providers

You can add your own placeholder resolvers by implementing IPlaceholderResolver. Potential sources could be REST APIs, files, secret stores etc...

How to add ConfigurationPlaceholders to your application

Different application setups require different ways to add ConfigurationPlaceholders.

WebApplication / minimal API

var builder = WebApplication.CreateBuilder( args );
builder
    .AddConfigurationPlaceholders( new InMemoryPlaceholderResolver( new Dictionary<String, String?>
    {
        { "FQDN", fullDomainName }
    } ) );

IHostBuilder / "old" ASP.NET / generic host

Host
    .CreateDefaultBuilder( args )
    .AddConfigurationPlaceholders( new InMemoryPlaceholderResolver( new Dictionary<String, String?>
    {
        { "FQDN", fullDomainName }
    } ) )

ConfigurationBuilder / console application

var configuration = new ConfigurationBuilder()
                    .AddJsonFile( "appsettings.json" )
                    ....
                    .AddConfigurationPlaceholders( new List<IPlaceholderResolver>
                    {
                        new InMemoryPlaceholderResolver( new Dictionary<String, String?>
                        {
                            {
                                "ApplicationName", Assembly.GetExecutingAssembly().GetName().Name
                            }
                        }
                        } ),
                        new EnvironmentVariableResolver()
                    } )
                    .Build();

Recursive placeholders

You can reference values containing placeholders from placeholders...

{
  "Lookup": {
    "SinksNs": "Serilog.Sinks",
    "DataDir": "X:/Temp/",
    "LogDir": "${Lookup:DataDir}logs/",
    "DbDir": "${Lookup:DataDir}db/"
  },
  "Serilog": {
    "Using": [ "${Lookup:SinksNs}.Console", "${Lookup:SinksNs}.File" ],
    "MinimumLevel": "Debug",
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": { "path": "${Lookup:LogDir}${ApplicationName}/${ApplicationName}-${ApplicationVersion}.log" }
      }
    ]
  },
  "Test": "Today is the ${Today} (${Day}) an it is ${Time} ${NoValueDefinedForThisOne}",
  "LocalDb": "${Lookup:DbDir}store.db"
}

In this example we can see several placeholders referencing values containing other placeholders.
E.g. ${Lookup:DbDir} will be resolved with the value ${Lookup:DataDir}db/ from Lookup:DbDir (using ConfigurationPlaceholderResolver). ${Lookup:DataDir} is another placeholder which will be replaced with the value of Lookup:DataDir => X:/Temp/.

You can combine values from multiple IPlaceholderResolver with multiple configuration sources.