0% found this document useful (0 votes)
26 views20 pages

App Settings Demystified (C# & VB)

Uploaded by

Leonardo Forero
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF or read online on Scribd
0% found this document useful (0 votes)
26 views20 pages

App Settings Demystified (C# & VB)

Uploaded by

Leonardo Forero
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF or read online on Scribd
.NET App Settings Demystified (C# & VB) Graeme_Grant 26 Feb 2023 CPOL Enabling development and production AppSettings support for [Link] Core apps In this article, you will learn how to enable development and production AppSettings support for non- [Link] Core Applications - Console, Winforms, and WPF - C# & VB samples included Download AppSettings v1.20 - 181.2 KB [NEW] Download AppSettings v1.11 - 178 KB [OBSOLETE] Download AppSettings v1.01 - 166.6 KB [OBSOLETE] Contents Introduction How Does This Work? How the Settings are Merged at Runtime Configuring for Development & Production Environments The Settings Configuration Setup © File: ‘appsettings,json’ © File: ‘appsettings Developmentjson’ © File: ‘[Link] json’ * Implementation © Settings © Sample Console Application (Dependency Injection) = 1. Setting up Dependency Injection = 2. Getting the Options = 3, Retrieving Individual Values © Sample Console Ap © How to Test © Conclusion # References © History tion (No Dependency Injection) [New] Introduction [Link] applications support Development & Production settings out of the box with appsettings,json via environment-driven multiple files - appsettings,ison, [Link],json, and [Link],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,son to other applications types, specifically Console, Winforms, and WPF types. Whilst not critical, to get a better understanding, we will look into the .NET source code and see how rosoft’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. 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 [Link] in HostingHostBuilderExtens ions class: ce internal static void ApplyDefaultHostConfiguration (IConfigurationBuilder hostConfigBuilder, string[]? args) t y* 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 CWO which is not Likely to really be the home for things Like [Link], we skip changing the ContentRoot in that case. The non-"default" initial value for ContentRoot is [Link] (e.g. the executable path) which probably makes more sense than the system32. In my testing, both [Link] 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 end = [Link]; if (![Link]([Link]) || [Link](cud, Environment .GetFolderPath (Environment .[Link]), ‘[Link])) t hostConfigSuilder AddInMemoryCollection(new[ ] { new KeyValuePair([Link], cud), y3 } hostConfigBuilder. AddEnvironmentVariables(prefix: "DOTNET_"); if (args is { Length: > @ }) t ? hostConfigBuilder AddConmandLine(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# HII //1 Provides information about the hosting environment an application is running in. Jif public interface IHostEnvironment { JI //1 Gets or sets the nane of the environment. /// The host automatically sets this property //1 to the value of the “environment” key as specified in configuration. II string EnvironnentName { get; set; } JI J/1 Gets or sets the name of the application. //1 This property is automatically set by the J/1 host to the assembly containing the application entry point. W/1 string ApplicationName { get; set; } JI J// Gets or sets the absolute path to the directory that contains J/1 the application content files. W/1 string ContentRootPath { get; set; } IIL. /// Gets or sets an pointing at ut ‘ContentRootPath"/>. WIL IFileProvider ContentRootFileProvider { get; set; } So the suffix is Environment. So the complete environment variable name is DOTNET_ENVIRONMENT. Unlike [Link] 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, [Link],json for both Debug and Release modes even if the appsettings. Developmentson exists. [Link],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 sAddJsonFile("appsettings. json", optional: true, reloadOnchange: reloadOnchange) -AddJsonFile($"appsettings.{[Link]}. json", optional: true, reloadOnChange: reloadOnChange); Lf ([Link]() && [Link] is { Length: > @ }) { var appAssenbly = [Link](new AssenblyName([Link])) ; if (appAssenbly is not null) t [Link](appAssenbly, optional: true, reloadonchange: reloadonChange) ; } [Link](); if (args is { Length: > @ }) t ? appConfigBuilder AddConmandLine(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. {[Link]}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 son 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. aoe Debug — General > Code eas atk bem a oD Or we can access the Launch Profiles window from the toolbar: > Staging ~| tf P Staging | Ws ane |v Staging Production 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. ‘Command tine arguments i stasing lan Ses (1 tects tat he cebugger shld tat toa proces ona emote aching ‘evionmentvarabler [BORNE RORNENT] Demian —] x 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] “conmandName”: "Project", { “environmentVariables "DOTNET_ENVIRONMENT' 3 + + ? "DEVELOPMENT" NOTE: The launchSettings son 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,son - settings used during development * [Link],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 * Asal 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 [Link],son 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: P Staging ~ P Staging WSL Development Staging Production AppSettings Debug Properties File: 'appsettings,json’ JavaScript "Default": "Information" File: '[Link]’ JavaScript “Logging”: “Loglevel” "Default": "Trace", “[Link] .[Link]” : } ? + File: '[Link]’ JavaScript “Logging”: “Logtevell "Default": "Warning", “[Link] Http. HttpClient If only appsettings json, then logging will be at least Information level. If in Debug mode, then appsettings. [Link] 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 [Link] 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 ce public class MySectionOptions { public string? Setting { get; set; } public int? Setting? { get; set; } 3 [Link] Public Class MySectionOptions Property Settingl As String Property Setting2 As Integer End Class 2. Add to the [Link] files: Now set options for the different configurations. 1. appsettings,json file: JavaScript { “MySection": { etting1": "default_value", “setting2": 222 3 + 2. appsettings. Development,json file: JavaScript MySection": { settingl": "development_value_1", etting2": 111 3. [Link],son file JavaScript ‘production_value_1", 333 Sample Console Application (Dependency Injection) Now we can read the options from the appsett ings: 1. Setting up Dependency Injection cH IHostBuilder builder = [Link](); // Map the options class to the section in the ~appsettings” builder. ConfigureServices( (context, services) => t Configuration configRoot = [Link]; services .Configure (configRoot .GetSection("MySection")); 3 IHost host - [Link](); [Link] Dim builder As IHostBuilder = [Link]() " Map the options class to the section in the “appsettings builder. ConfigureServices( Sub(context, services) Dim configRoot As Configuration = [Link] [Link](Of MySectionOptions) (configRoot .GetSection("MySection")) End Sub) Dim host As IHost = [Link]() NOTES: We have defined that we will be retrieving strongly typed options section. 2. Getting the Options cH // If environment variable not set, will default to “Production” String env = [Link]("OOTNET_ENVIRONVENT") ?? "Production [Link]($"Environment: {env}"); // Get the options from ~appsettings® MySectionOptions options = [Link] -GetRequiredService() Values [Link]($"Settingl: {options.Setting1}"); [Link]($"Setting2: {options.Setting2}"); [Link](); [Link] " If environment variable not set, will default to "Production" Dim env As String = Environment .GetEnvironmentVariable("DOTNET_ENVIRONMENT") If [Link](env) Then env = “Productio End If [Link]($"Environment: {env}") ' Get the options from “appsettings Dim options As MySectionOptions = _host.Services _ -GetRequiredService(OF Ioptions(OF MySectionOptions)) _ Value [Link]($"Settingl: {options.Setting1}") [Link]($"Setting2: {options .Setting2}") [Link]() 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: cH // manually retrieve values Iconfiguration config = [Link](); // Log Level section [Link]("By Individual Loglevel key [Logging:LogLevel:Default]"); IConfigurationSection logLevel = [Link](" Logging: LogLevel: Default”); [Link]($" Loglevel: ([Link]}"); [Link](); // Option Settings section [Link]("By Individual Option keys"); IConfigurationSection setting = [Link]("MySection:settingt"); [Link]($" Settingl: ([Link])"); IconfigurationSection setting? = [Link]("MySection:setting2"); [Link]($" Setting2: ([Link]}"); [Link] manually retrieve values dim config As IConfiguration = _host.[Link](of IConfiguration) " Log Level section [Link]("8y Individual LogLevel key [Logging:LogLevel:befault]") dim loglevel = [Link]( "Logging: Logtevel:Default") [Link]($" LogLevel: ([Link]}") [Link]() ' option Settings section [Link]("By Individual option keys") dim setting1 As IConfigurationSection = [Link]("MySection:setting1") [Link]($" Settingl: {[Link]}") dim setting2 as IConfigurationSection = [Link]("MySection:setting2") [Link]($" Setting2: ([Link]}") 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 | have created a helper class to abstract away the code required to read the appsettings,json configuration information: The following NuGet packages are required: ‘© Microsoft.€[Link] * Microsoft.€[Link] ‘* Microsoft [Link] * [Link] c# public class AppSettings { region Constructors public AppSettings(IConfigurationSection configSection, string? key = null) t _configSection = configSection; Getvalue(key) 5 ? ftendregion region Fields protected static AppSettings? _appSetting; protected static IConfigurationSection? _configSection; ftendregion ltregion Properties public Toption? Value { get; set; } lwendregion region Methods| public static TOption? Current(string section, string? key = null) ‘ _appSetting = GetCurrentSettings(section, key); return _appSetting. Value; } public static AppSettings GetCurrentSettings(string section, string? key null) t string env = Environment .GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? Production” IconfigurationBuilder builder = new ConfigurationBuilder() -SetBasePath ([Link]()) sAddJsonFile("appsettings. json", optional: true, reloadonChange: true) -AddJsonFile($"appsettings.{env}.json", optional: true, reloadonChang -AddEnvironmentvariables(); IconfigurationRoot configuration = builder. Build(); if (string. IsNullorempty(section)) section = "AppSettings"; // default AppSettings settings = new AppSettings([Link](section), key); return settings; } protected virtual void GetValue(string? key) { if (key is null) t // no key, so must be a class/strut object Value = [Link](); _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)) 8& _configSection I= null) t // we must be retrieving a value Value = _configSection.GetValue (key); returns } // Could not find a supported type ‘throw new InvalidCastException($"Type {typeof (Toption).Name} is invalid } lwendregion } [Link] true) 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(0f Toption) Protected Shared _configSection As IConfigurationSection End Region Region "Properties" Public Property Value As Toption wend 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 [Link](env) Then env = "Production" End If Din builder As IConfigurationBuilder = New ConfigurationBuilder() _ -SetBasePath([Link]()) _ ‘Add)sonFile("[Link]", optional:=True, reloadonchang. ‘Add)sonFile($" appsettings. {env}. json", _ optional:=True, reloadonchange: -AddEnvironnentVariables() True) _ Dim configuration As IConfigurationRoot = [Link]() If String. IsNullorenpty(section) Then section = "AppSettings” * Default End If Dim settings As AppSettings(0F Toption) = _ New AppSettings(OF TOption) ([Link](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 = [Link](0f TOption) _configSection.Bind(Value) Return End TF Dim optiontype As Type = GetType(TOption) If (optiontype Is GetType(String) OrElse optionType Is GetType(Integer) Orelse optionType Is GetType(Long) Orélse optionType Is GetType(Decimal) Orelse optionType 1s GetType(Single) Orélse optiontype Is GetType(Double)) _ AndAlso _configSection IsNot Nothing Then we must be retrieving a value Value = _configSection.GetValue(0f 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: cH [Link]("By Individual Loglevel key [Logging:Loglevel:Default]"); string? loglevel = AppSettings.Current("Logging: LogLevel [Link]($" LogLevel: {loglevel}"); [Link](); [Link]("By Individual keys"); string? settingl = AppSettings.current("MySection", "Settingi"); Int? setting2 = AppSettings.Current("MySection", "Setting2"); [Link]($" [Link] ine($" [Link](); Settingl: (setting1}"); Setting2: (setting2}"); [Link]("By Option Class") MySectionOptions? options = AppSettings.Current("NySection”); [Link] ine($" [Link]($" [Link](); Settingl: {options?.Setting1) Setting2: {options?.Setting2}"); [Link] [Link]("By Individual Loglevel key [Logging:Loglevel :Default]") dim loglevel = AppSettings(OF String).Current("Logging:Loglevel", "Default") [Link]($" Loglevel: {logLevel}") [Link] ine() [Link]("By Individual keys") Dim settingt Dim setting2 AppSettings(Of String).Current("MySection", "Setting1") AppSettings(Of Integer).Current("MySection", "Setting2") [Link]($" [Link] ine($" [Link]() Settingl: {setting1}") Setting2: {setting2}") [Link]("By Option Class") Dim options = AppSettings (Of MySectionoptions).current("MySection") [Link]($" Settingl: {[Link])") [Link]($" Setting2: (options.Setting2}") [Link]() 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: Environnent: Production By Individual LogLevel key [Logging: LogLevel:Default] Loglevel: Information By Individual Option keys Settingl: default_value_1 Setting2: 222 By Option Class Settingl: 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 Settingl: production_value_1 Setting2: 333 By Option Class Settingl: 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 Settingl: development_value_1 Setting2: 111 By Option Class Settingl: 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,son 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 Setting]: production_value_1 Setting2: 333 By Option Class Settingl: 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 [Link] 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 © Options pattern in NET Configuration providers in .NET | Microsoft Learn .NET Generic Host | Microsoft Learn [Link] | Github - dotnet/dotnet Article Image courtesy of: How to Photograph a Rocket Launch at Night History + 12%" February, 2023 - v1.00 - Initial release * 14th February, 2023 - v1.10 - Added section Sample Console Application (No Dependency Injection) using AppSettings helper class © 16!" February, 2023 - v1.11 - optimized appSettings helper class © 25! 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 License This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL) Written By Graeme_Grant Technical Lead Australia This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming. Comments and Discussions © 6 messages have been posted for this article Visit [Link] to post and view comments on this article, or click here to get a print view with messages. Permalink Article Copyright 2023 by Graeme Grant Advertise Everything else Copyright © CodeProject, Privacy 1999-2023, Cookies Terms of Use Web02 2.8:2023-01-23:1

You might also like