P3.NET

Strongly Typed Application Settings Using T4

We all know that the Settings infrastructure added to .NET a while back is the “correct” way to create strongly typed application settings in configuration files. The problem with this approach though is that the entries generated in the config file aren’t pretty. You have only limited control over the naming, there are lots of extra information to wade through and non-devs can easily get confused. The Appsettings that has existed since v1 is a simpler approach that most people are comfortable with. But you lose a lot of functionality by using AppSettings. The Settings infrastructure brings into play a Settings class with strongly typed properties for the settings and default values for settings that are missing. But sometimes that is overkill or simplicity is just too important.

Update 28 Aug 2016: Refer to this link for updated code supporting Visual Studio 2015.

Fortunately the biggest benefit, strongly typed properties, can be easily implemented over the existing AppSettings infrastructure. With a little bit of
extra code you can even deduce the type of the settings. This gives developers the benefits of type checking and compiler validation with the ease of configuration. To get all this to work all you need to do is create a T4 template. A full discussion of writing T4 templates is beyond the scope of this post so I’m assuming you already have basic knowledge. For more information on T4 templates I recommend MSDN and some of the blogs on the topic.

  • MSDN
  • Scott Hanselman’s blog on T4 with links to lots of information

The goal is to create a Settings class that wraps each of the settings defined in appSettings in the config file. Rather than calling ConfigurationManager with some string property that might or might not exist we want to reference a strongly typed property that is enforced at compile time. If someone removes a setting from the config file we want a compiler error to let us know. It also makes refactoring and determining who is using the setting a lot easier. Here’s some reasonable goals:

  1. Create a static class to expose each setting defined in appSettings as a strongly typed, readonly property.
  2. Use the most restrictive type for each property rather than string.
  3. Update the generated class whenever the configuration file changes during development.

There are many articles out there on how to write T4 templates so I’m not going to go into the details other than to point out a few interesting tidbits. Also note that some of the code I’m using was inspired from the T4 work that others have done but unfortunately it has been so long that I cannot remember who all I should give credit due.

Defining the template, finding the namespace to use and locating the configuration file are pretty standard stuff. Here’s the start of the template.

namespace <#= GetNamespace()#>
{
   internal class AppSettings
   {
<#
      ExeConfigurationFileMap configFile = new ExeConfigurationFileMap();
      configFile.ExeConfigFilename = GetConfigPath();

	  if (String.IsNullOrEmpty(configFile.ExeConfigFilename))
	     throw new ArgumentNullException("The project does not contain App.config or Web.config file.");
		 
      var config = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(configFile, ConfigurationUserLevel.None);
#>
    }
}

GetNamespace and GetConfigPath are contained in a separate .ttinclude file that can be reused across templates. They do what you might expect. Code in the <# #> block runs when the template is evaluated and can contain any C# code. For this template the code creates a Configuration object for the config file used by the project. This will be used to enumerate the application settings later. The key functionality for this template is to enumerate the app settings which we can do almost like we’d do in regular code.

   var appSettings = config.AppSettings.Settings;
<#
   foreach (KeyValueConfigurationElement setting in appSettings) {
#>
   public static <#= GetSettingType(setting.Value) #> <#= GetSafeName(setting.Key) #>
   {
      get 
	  { 
	     return <#= GetSettingConverter(setting.Value) #>(GetConfigSetting("<#= setting.Key #>")); 
      }
   }
<# 
   }
#>

   private static string GetConfigSetting ( string settingName )
   {
      var setting = ConfigurationManager.AppSettings[settingName];
      return setting ?? "";
   }
}

For each setting the template generates a new, readonly property that returns the value from the configuration subsystem at runtime. This is done through a helper method on the class. <#= %> tells the generator to evaluate the expression at generation time store the results in the template. So the setting key is baked into the code at generation time but the retrieval of the setting is deferred until runtime.

The trickier part is determining what type to use for the setting. That is what the GetSettingConverter is for. It is evaluated at generation time to determine the best type for the setting. In my development the config file generally contains the default values to use for settings. Either at deployment or via transforms the settings can be changed to match the needs of the environment. Because the config file contains default values it makes sense that we can try to deduce the type based upon the default value. To keep things simple we’ll just stick with simple types like numbers, booleans and strings. We could certainly get more complex but I don’t think that is necessary. If no type can be determined then string is used.

<#@includefile="StandardTemplateInclude.ttinclude" #>
<#+ 
private static string GetSettingConverter ( string value )
{
   var type = GetSettingType(value);

   if (type == "int") return "Convert.ToInt32";
   else if (type == "double") return "Convert.ToDouble";
   else if (type == "bool") return "Convert.ToBoolean"; 
   
   return ""
}

private static string GetSettingType ( string value )
{
   //Try to convert to int first
   int iValue;
   if (Int32.TryParse(value, out iValue))
      return "int";
	 
   //Double is next
   double dValue;
   if (Double.TryParse(value, out dValue))
      return "double";

   //Boolean
   bool bValue;
   if (Boolean.TryParse(value, out bValue))
      return "bool";

   return "string";
}
#>

First the standard include file is added. The include file contains the helper methods mentioned earlier. In T4 these are known as class functions (callable at generation time) so they must appear after all the other blocks. The rest of the template consists of the class functions used to determine the setting type. GetSettingConverter takes the value of the setting as was stored in the config file and attempts to parse it to determine its type. The GetSettingType method takes a simple approach of just attempting to convert the setting to various types until it succeeds. If you want to support more types or add better logic then this is the method to change. If the type cannot be determined then string is used. It is important to note that the return value is the primitive type name because this will end up being the value that is used in the property signature. The converter method then generates a call to the appropriate converter function to convert the string value, stored in the config file, to the appropriate type, at runtime. For this sample the standard conversion routines are used but more likely you’ll want to call specialized conversion routines that can handle invalid values.

Having implemented all this we can run the template (by right clicking and selecting Run Custom Tool) and a generated file should appear with the strongly typed properties. The last thing that needs to happen is that the template needs to be updated whenever the config file changes. While templates can be configured to refresh when certain files change I’ve had bad luck getting it to work correctly. Therefore I just know that if I add a new setting to the config file (that I want programmatic access to) then I need to manually refresh the generated file. While not ideal it is at least simple.

Attached to this post is the source code for the template I’m using. It could probably be improved in many ways but for me it solves the problem at hand. Feel free to use it if you need to.

Download the Code