Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

.NET App Settings Demystified (C# & VB)

5.00/5 (13 votes)
25 Feb 2023CPOL7 min read 35.2K   494  
Enabling development and production AppSettings support for non-ASP.NET Core apps
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:

C#
internal static void ApplyDefaultHostConfiguration
         (IConfigurationBuilder hostConfigBuilder, string[]? args)
{
    /*
       If we're running anywhere other than C:\Windows\system32, 
       we default to using the CWD for the ContentRoot. 
       However, since many things like Windows services and MSIX installers have 
       C:\Windows\system32 as there CWD which is not likely to really be 
       the home for things like appsettings.json, we skip 
       changing the ContentRoot in that case. The non-"default" initial
       value for ContentRoot is AppContext.BaseDirectory 
       (e.g. the executable path) which probably
       makes more sense than the system32.

       In my testing, both Environment.CurrentDirectory and Environment.GetFolderPath(
       Environment.SpecialFolder.System) return the path without 
       any trailing directory separator characters. I'm not even sure 
       the casing can ever be different from these APIs, but I think
       it makes sense to ignore case for Windows path comparisons 
       given the file system is usually
       (always?) going to be case insensitive for the system path.
     */

    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:

C#
/// <summary>
/// Provides information about the hosting environment an application is running in.
/// </summary>
public interface IHostEnvironment
{
    /// <summary>
    /// Gets or sets the name of the environment. 
    /// The host automatically sets this property
    /// to the value of the "environment" key as specified in configuration.
    /// </summary>
    string EnvironmentName { get; set; }

    /// <summary>
    /// Gets or sets the name of the application. 
    /// This property is automatically set by the
    /// host to the assembly containing the application entry point.
    /// </summary>
    string ApplicationName { get; set; }

    /// <summary>
    /// Gets or sets the absolute path to the directory that contains 
    /// the application content files.
    /// </summary>
    string ContentRootPath { get; set; }

    /// <summary>
    /// Gets or sets an <see cref="IFileProvider"/> pointing at 
    /// <see cref="ContentRootPath"/>.
    /// </summary>
    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:

C#
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:

C#
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.

Image 1

Or we can access the Launch Profiles window from the toolbar:

Image 2

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.

Image 3

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:

JavaScript
{
  "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:

Image 4

File: 'appsettings.json'

JavaScript
{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

File: 'appsettings.Development.json'

JavaScript
{
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "System.Net.Http.HttpClient": "Trace"
    }
  }
}

File: 'appsettings.Production.json'

JavaScript
{
  "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:

  1. Our Settings Options class:
    C#
    public class MySectionOptions
    {
       public string? Setting1 { get; set; }
    
       public int? Setting2 { get; set; }
    }
    VB.NET
    Public Class MySectionOptions
    
       Property Setting1 As String
    
       Property Setting2 As Integer
    
    End Class
  2. Add to the appsettings files:

    Now set options for the different configurations.

    1. appsettings.json file:
      JavaScript
      {
        "MySection": {
          "setting1": "default_value_1",
          "setting2": 222
        }
      }
    2. appsettings.Development.json file:
      JavaScript
      {
        "MySection": {
          "setting1": "development_value_1",
          "setting2": 111
        }
      }
    3. appsettings.Production.json file:
      JavaScript
      {
        "MySection": {
          "setting1": "production_value_1",
          "setting2": 333
        }
      }
NOTES: 
* We have set up strongly-typed access to the configuration for using the Options Pattern. For more information, please read this article: Options Pattern in .Net | Microsoft Learn

Sample Console Application (Dependency Injection)

Now we can read the options from the appsettings:

1. Setting up Dependency Injection

C#
IHostBuilder builder = Host.CreateDefaultBuilder();

// Map the options class to the section in the `appsettings`
builder.ConfigureServices((context, services) =>
{
    IConfiguration configRoot = context.Configuration;

    services.Configure<MySectionOptions>
        (configRoot.GetSection("MySection"));
});

IHost host = builder.Build();
VB.NET
Dim builder As IHostBuilder = Host.CreateDefaultBuilder()

' Map the options class to the section in the `appsettings`
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

C#
// If environment variable not set, will default to "Production"
string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
Console.WriteLine($"Environment: {env}");

// Get the options from `appsettings`
MySectionOptions options = host.Services
    .GetRequiredService<IOptions<MySectionOptions>>()
    .Value;

Console.WriteLine($"Setting1: {options.Setting1}");
Console.WriteLine($"Setting2: {options.Setting2}");
Console.ReadKey();
VB.NET
' If environment variable not set, will default to "Production"
Dim env As String = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")

If String.IsNullOrWhiteSpace(env) Then
    env = "Production"
End If

Console.WriteLine($"Environment: {env}")

' Get the options from `appsettings`
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:

C#
// manually retrieve values
IConfiguration config = host.Services.GetRequiredService<IConfiguration>();

// Log Level section
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]");

IConfigurationSection logLevel = config.GetSection("Logging:LogLevel:Default");

Console.WriteLine($"  LogLevel: {logLevel.Value}");
Console.WriteLine();

// Option Settings section
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}");
VB.NET
' manually retrieve values
dim config As IConfiguration = _host.Services.GetRequiredService(of IConfiguration)

' Log Level section
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]")

dim logLevel = config.GetSection("Logging:LogLevel:Default")

Console.WriteLine($"  LogLevel: {logLevel.Value}")
Console.WriteLine()

' Option Settings section
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:

C#
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"; // default

        AppSettings<TOption> settings =
            new AppSettings<TOption>(configuration.GetSection(section), key);

        return settings;
    }

    protected virtual void GetValue(string? key)
    {
        if (key is null)
        {
            // no key, so must be a class/strut object
            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)
        {
            // we must be retrieving a value
            Value = _configSection.GetValue<TOption>(key);
            return;
        }

        // Could not find a supported type
        throw new InvalidCastException($"Type {typeof(TOption).Name} is invalid");
    }

    #endregion
}
VB.NET
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" ' Default
        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

            ' no key, so must be a class/strut object
            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

            ' we must be retrieving a value
            Value = _configSection.GetValue(Of TOption)(key)
            Return

        End If

        ' Could not find a supported type
        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:

C#
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();
VB.NET
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.

  1. 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
  2. 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
  3. 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
  4. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)