In this article, you will learn how to enable development and production AppSettings support for non-ASP.NET Core Applications - Console, Winforms, and WPF - C# & VB samples included
Contents
Introduction
ASP.NET applications support Development & Production settings out of the box with appsettings.json via environment-driven multiple files - appsettings.json, appsettings.Development.json, and appsettings.Production.json.
appsettings.json is just one of the many places where application settings can be set. You can read more about this here: Configuration providers in .NET | Microsoft Learn
This article will focus on adding support for appsettings.json to other application types, specifically Dot Net Core Console, Winforms, and WPF types.
Whilst not critical, to get a better understanding, we will look into the Dot Net Core Framework source code and see how Microsoft's configuration works. What we will cover is documented in the link above. Then we will do a quick test in a Console
application to better understand how and why.
NOTE: This is purely an article focused on Dot Net Core, not .Net Framework.
How Does This Work?
We need to look at how the framework wires up the appsettings.json. To do this, we need to explore the implementation in the framework code, specifically the Hostbuilder.ConfigureDefaults
in HostingHostBuilderExtensions
class:
internal static void ApplyDefaultHostConfiguration
(IConfigurationBuilder hostConfigBuilder, string[]? args)
{
string cwd = Environment.CurrentDirectory;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
!string.Equals(cwd, Environment.GetFolderPath
(Environment.SpecialFolder.System),
StringComparison.OrdinalIgnoreCase))
{
hostConfigBuilder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string?>(HostDefaults.ContentRootKey, cwd),
});
}
hostConfigBuilder.AddEnvironmentVariables(prefix: "DOTNET_");
if (args is { Length: > 0 })
{
hostConfigBuilder.AddCommandLine(args);
}
}
Here, we can see that an Environment
variable with the prefix DOTNET_
is used.
You can read more about this here: .NET Generic Host | Microsoft Learn.
To get the suffix, we look at the comments in IHostEnvironment
for the EnvironmentName
property:
public interface IHostEnvironment
{
string EnvironmentName { get; set; }
string ApplicationName { get; set; }
string ContentRootPath { get; set; }
IFileProvider ContentRootFileProvider { get; set; }
}
So the suffix is Environment
. So the complete environment variable name is DOTNET_ENVIRONMENT
. Unlike ASP.NET and its ASPNETCORE_ENVIRONMENT
variable, the DOTNET_ENVIRONMENT
is not set by default.
When there is no environment variable set, the EnviromentName
defaults to Production
and, if it exists, appsettings.Production.json for both Debug
and Release
modes even if the appsettings.Development.json exists. appsettings.Development.json will be ignored.
How the Settings are Merged at Runtime
We need to look at another extension method in HostingHostBuilderExtensions
class:
internal static void ApplyDefaultAppConfiguration(
HostBuilderContext hostingContext,
IConfigurationBuilder appConfigBuilder, string[]? args)
{
IHostEnvironment env = hostingContext.HostingEnvironment;
bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
appConfigBuilder
.AddJsonFile("appsettings.json", optional: true,
reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: reloadOnChange);
if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly is not null)
{
appConfigBuilder.AddUserSecrets(appAssembly, optional: true,
reloadOnChange: reloadOnChange);
}
}
appConfigBuilder.AddEnvironmentVariables();
if (args is { Length: > 0 })
{
appConfigBuilder.AddCommandLine(args);
}
What we are interested in is this:
appConfigBuilder.AddJsonFile("appsettings.json", optional: true,
reloadOnChange: reloadOnChange) .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: reloadOnChange);
Here, we can see that the appsettings.json is loaded first, then the $"appsettings.{env.EnvironmentName}.json" where the EnvironmentName
is dependent on the DOTNET_ENVIRONMENT
environment variable. As we know from above, unless we manually choose one, EnvironmentName
will default to Production
.
Any settings in the appsettings.json will be overridden by the appsettings.Production.json values if set.
Configuring for Development & Production Environments
To set the DOTNET_ENVIRONMENT
variable, open the application 'Properties' by right-clicking on the application name in the Solution Explorer, navigate to Debug > General section and click on the 'open debug launch profiles UI'. This will open the Launch Profile window.
Or we can access the Launch Profiles window from the toolbar:
What we are interested in is the Environment variables
. You can set anything here. What we need to add is the name: DOTNET_ENVIRONMENT
with value: Development
. There is no close button, simply close the window and chose File > Save... from the VS menu.
What this has done is added a new folder Properties to the root of the solution folder and created the launchSettings.json file with the following:
{
"profiles": {
"[profile_name_goes_here]": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "DEVELOPMENT"
}
}
}
}
NOTE: The launchSettings.json file is only applicable to Visual Studio. It will not be copied to your compiled folder. If you do include it with your application, the runtime will ignore it. You will need to manually support this file in your application for use outside of Visual Studio.
The Settings Configuration Setup
We want different settings for development and production, so we configure the files:
- appsettings.json - this holds the root configuration for both environments
- appsettings.Development.json - settings used during development
- appsettings.Production.json - settings used for deployed live applications
NOTE
- All appsettings files need to be marked as Content and Copy if newer to be used in both Debug and Release modes
- As all appsettings files will be in the Release folder, you will need to configure the installer to only package the files required - do not include the appsettings.Development.json file.
You can set up multiple Launch Profiles. Above, I have set three (3), each with its own environment variable setting:
- Development -
Development
- Staging -
Staging
- Production - none (defaults to
Production
)
To select a profile to test, we can select from the Toolbar:
File: 'appsettings.json'
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
File: 'appsettings.Development.json'
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"System.Net.Http.HttpClient": "Trace"
}
}
}
File: 'appsettings.Production.json'
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"System.Net.Http.HttpClient": "Warning"
}
}
}
If only appsettings.json, then logging will be at least Information
level.
If in Debug mode, then appsettings.Development.json with all logging enabled.
If in Release mode, then appsettings.Production.json, logging for Warning
, Error
, and Critical
logging.
Same applies to other application options set in the appsettings.json file(s).
Implementation
We will be creating a Console
application to check out what was learned above. We will use sample options in appsettings
and map them to an options class. The code is minimal to keep the implementation simple to understand.
Settings
We need to prepare the options. We will require a class to map the option section settings from the appsettings file:
- Our Settings Options class:
public class MySectionOptions
{
public string? Setting1 { get; set; }
public int? Setting2 { get; set; }
}
Public Class MySectionOptions
Property Setting1 As String
Property Setting2 As Integer
End Class
- Add to the
appsettings
files:
Now set options for the different configurations.
- appsettings.json file:
{
"MySection": {
"setting1": "default_value_1",
"setting2": 222
}
}
- appsettings.Development.json file:
{
"MySection": {
"setting1": "development_value_1",
"setting2": 111
}
}
- appsettings.Production.json file:
{
"MySection": {
"setting1": "production_value_1",
"setting2": 333
}
}
NOTES:
Sample Console Application (Dependency Injection)
Now we can read the options from the appsettings
:
1. Setting up Dependency Injection
IHostBuilder builder = Host.CreateDefaultBuilder();
builder.ConfigureServices((context, services) =>
{
IConfiguration configRoot = context.Configuration;
services.Configure<MySectionOptions>
(configRoot.GetSection("MySection"));
});
IHost host = builder.Build();
Dim builder As IHostBuilder = Host.CreateDefaultBuilder()
builder.ConfigureServices(
Sub(context, services)
Dim configRoot As IConfiguration = context.Configuration
services.Configure(Of MySectionOptions)(configRoot.GetSection("MySection"))
End Sub)
Dim _host As IHost = builder.Build()
NOTES: We have defined that we will be retrieving the strongly-typed options section for using the Options Pattern.
2. Retrieving Strongly-Typed Options
string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
Console.WriteLine($"Environment: {env}");
MySectionOptions options = host.Services
.GetRequiredService<IOptions<MySectionOptions>>()
.Value;
Console.WriteLine($"Setting1: {options.Setting1}");
Console.WriteLine($"Setting2: {options.Setting2}");
Console.ReadKey();
Dim env As String = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
If String.IsNullOrWhiteSpace(env) Then
env = "Production"
End If
Console.WriteLine($"Environment: {env}")
Dim options As MySectionOptions = _host.Services _
.GetRequiredService(Of IOptions(Of MySectionOptions)) _
.Value
Console.WriteLine($"Setting1: {options.Setting1}")
Console.WriteLine($"Setting2: {options.Setting2}")
Console.ReadKey()
3. Retrieving Individual Values
Above, we looked at how we retrieve a strongly typed section. What if we are only interested in an individual Key-Value pair. We can do that too:
IConfiguration config = host.Services.GetRequiredService<IConfiguration>();
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]");
IConfigurationSection logLevel = config.GetSection("Logging:LogLevel:Default");
Console.WriteLine($" LogLevel: {logLevel.Value}");
Console.WriteLine();
Console.WriteLine("By Individual Option keys");
IConfigurationSection setting1 = config.GetSection("MySection:setting1");
Console.WriteLine($" Setting1: {setting1.Value}");
IConfigurationSection setting2 = config.GetSection("MySection:setting2");
Console.WriteLine($" Setting2: {setting2.Value}");
dim config As IConfiguration = _host.Services.GetRequiredService(of IConfiguration)
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]")
dim logLevel = config.GetSection("Logging:LogLevel:Default")
Console.WriteLine($" LogLevel: {logLevel.Value}")
Console.WriteLine()
Console.WriteLine("By Individual Option keys")
dim setting1 As IConfigurationSection = config.GetSection("MySection:setting1")
Console.WriteLine($" Setting1: {setting1.Value}")
dim setting2 as IConfigurationSection = config.GetSection("MySection:setting2")
Console.WriteLine($" Setting2: {setting2.Value}")
NOTES: To read individual key-value pairs, we get a reference to the DI Options Configuration, then we retrieve the information.
Sample Console Application (No Dependency Injection)
Not everyone uses Dependency Injection, so I have created a helper class to abstract away the code required to read the appsettings.json configuration information:
The following NuGet packages are required:
public class AppSettings<TOption>
{
#region Constructors
public AppSettings(IConfigurationSection configSection, string? key = null)
{
_configSection = configSection;
GetValue(key);
}
#endregion
#region Fields
protected static AppSettings<TOption>? _appSetting;
protected static IConfigurationSection? _configSection;
#endregion
#region Properties
public TOption? Value { get; set; }
#endregion
#region Methods
public static TOption? Current(string section, string? key = null)
{
_appSetting = GetCurrentSettings(section, key);
return _appSetting.Value;
}
public static AppSettings<TOption>
GetCurrentSettings(string section, string? key = null)
{
string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ??
"Production";
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
IConfigurationRoot configuration = builder.Build();
if (string.IsNullOrEmpty(section))
section = "AppSettings";
AppSettings<TOption> settings =
new AppSettings<TOption>(configuration.GetSection(section), key);
return settings;
}
protected virtual void GetValue(string? key)
{
if (key is null)
{
Value = Activator.CreateInstance<TOption>();
_configSection.Bind(Value);
return;
}
Type optionType = typeof(TOption);
if ((optionType == typeof(string) ||
optionType == typeof(int) ||
optionType == typeof(long) ||
optionType == typeof(decimal) ||
optionType == typeof(float) ||
optionType == typeof(double))
&& _configSection != null)
{
Value = _configSection.GetValue<TOption>(key);
return;
}
throw new InvalidCastException($"Type {typeof(TOption).Name} is invalid");
}
#endregion
}
Public Class AppSettings(Of TOption)
#Region "Constructors"
Public Sub New(configSection As IConfigurationSection, _
Optional key As String = Nothing)
_configSection = configSection
GetValue(key)
End Sub
#End Region
#Region "Fields"
Protected Shared _appSetting As AppSettings(Of TOption)
Protected Shared _configSection As IConfigurationSection
#End Region
#Region "Properties"
Public Property Value As TOption
#End Region
#Region "Methods"
Public Shared Function Current(section As String, _
Optional key As String = Nothing) As TOption
_appSetting = GetCurrentSettings(section, key)
Return _appSetting.Value
End Function
Public Shared Function GetCurrentSettings(section As String, _
Optional key As String = Nothing) As AppSettings(Of TOption)
Dim env As String = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
If String.IsNullOrWhiteSpace(env) Then
env = "Production"
End If
Dim builder As IConfigurationBuilder = New ConfigurationBuilder() _
.SetBasePath(Directory.GetCurrentDirectory()) _
.AddJsonFile("appsettings.json", optional:=True, reloadOnChange:=True) _
.AddJsonFile($"appsettings.{env}.json", _
optional:=True, reloadOnChange:=True) _
.AddEnvironmentVariables()
Dim configuration As IConfigurationRoot = builder.Build()
If String.IsNullOrEmpty(section) Then
section = "AppSettings"
End If
Dim settings As AppSettings(Of TOption) = _
New AppSettings(Of TOption)(configuration.GetSection(section), key)
Return settings
End Function
Protected Overridable Sub GetValue(Optional key As String = Nothing)
If key Is Nothing Then
Value = Activator.CreateInstance(Of TOption)
_configSection.Bind(Value)
Return
End If
Dim optionType As Type = GetType(TOption)
If (optionType Is GetType(String) OrElse
optionType Is GetType(Integer) OrElse
optionType Is GetType(Long) OrElse
optionType Is GetType(Decimal) OrElse
optionType Is GetType(Single) OrElse
optionType Is GetType(Double)) _
AndAlso _configSection IsNot Nothing Then
Value = _configSection.GetValue(Of TOption)(key)
Return
End If
Throw New InvalidCastException($"Type {GetType(TOption).Name} is invalid")
End Sub
#End Region
End Class
NOTE
- The
AppSettings
helper class works with Option
classes and individual key values.
Using the helper class is very simple:
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]");
string? logLevel = AppSettings<string>.Current("Logging:LogLevel", "Default");
Console.WriteLine($" LogLevel: {logLevel}");
Console.WriteLine();
Console.WriteLine("By Individual keys");
string? setting1 = AppSettings<string>.Current("MySection", "Setting1");
int? setting2 = AppSettings<int>.Current("MySection", "Setting2");
Console.WriteLine($" Setting1: {setting1}");
Console.WriteLine($" Setting2: {setting2}");
Console.WriteLine();
Console.WriteLine("By Option Class");
MySectionOptions? options = AppSettings<MySectionOptions>.Current("MySection");
Console.WriteLine($" Setting1: {options?.Setting1}");
Console.WriteLine($" Setting2: {options?.Setting2}");
Console.ReadKey();
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]")
dim logLevel = AppSettings(Of String).Current("Logging:LogLevel", "Default")
Console.WriteLine($" LogLevel: {logLevel}")
Console.WriteLine()
Console.WriteLine("By Individual keys")
Dim setting1 = AppSettings(Of String).Current("MySection", "Setting1")
Dim setting2 = AppSettings(Of Integer).Current("MySection", "Setting2")
Console.WriteLine($" Setting1: {setting1}")
Console.WriteLine($" Setting2: {setting2}")
Console.WriteLine()
Console.WriteLine("By Option Class")
Dim options = AppSettings(Of MySectionOptions).Current("MySection")
Console.WriteLine($" Setting1: {options.Setting1}")
Console.WriteLine($" Setting2: {options.Setting2}")
Console.ReadKey()
How to Test
We are going to gradually configure the settings to see how they work in both Debug
and Release
modes.
- Testing with appsettings.json file only:
Environment: Production
By Individual LogLevel key [Logging:LogLevel:Default]
LogLevel: Information
By Individual Option keys
Setting1: default_value_1
Setting2: 222
By Option Class
Setting1: default_value_1
Setting2: 222
-
Now include the appsettings.Production.json file:
Environment: Production
By Individual LogLevel key [Logging:LogLevel:Default]
LogLevel: Warning
By Individual Option keys
Setting1: production_value_1
Setting2: 333
By Option Class
Setting1: production_value_1
Setting2: 333
-
Now include the appsettings.Develpment.json file:
Environment: Production
By Individual LogLevel key [Logging:LogLevel:Default]
LogLevel: Warning
By Individual Option keys
Setting1: production_value_1
Setting2: 333
By Option Class
Setting1: production_value_1
Setting2: 333
-
Set the launchSetting.json file:
Environment: Development
By Individual LogLevel key [Logging:LogLevel:Default]
LogLevel: Trace
By Individual Option keys
Setting1: development_value_1
Setting2: 111
By Option Class
Setting1: development_value_1
Setting2: 111
But wait, for test 4. in Release
mode, and started without debugging, we still see Environment: Development
. This is because of the launchSetting.json environment variable.
We need to go to the command line and run the application in the bin\Release\net7.0 folder, then we will see the following output:
Environment: Production
By Individual LogLevel key [Logging:LogLevel:Default]
LogLevel: Warning
By Individual Option keys
Setting1: production_value_1
Setting2: 333
By Option Class
Setting1: production_value_1
Setting2: 333
Or we can set up another Launch Profile to emulate Production / Release mode - see Configuring for Development & Production Environments.
Conclusion
Whilst ASP.NET Core has this configured out of the box, we can add the same behavior to our own Console, Winforms, or WPF app by adding our own environment variable in launchsettings.json, then set up the appsettings files.
References
History
- 12th February, 2023 - v1.00 - Initial release
- 14th February, 2023 - v1.10 - Added section Sample Console Application (No Dependency Injection) using
AppSettings
helper class - 16th February, 2023 - v1.11 - optimized
AppSettings
helper class - 25th February, 2023 - v1.20 - Added documentation and updated sample code to demonstrate working with individual key-value pairs with Dependency Injection and how to work with nested sections
- 28th February, 2023 - v1.21 - Added clarification where needed regarding the use of the Options pattern