P3.NET

Using T4 to Create an AppSettings Wrapper, Part 3

In the previous article we expanded the template to dynamically set the type and namespace names based upon the project the template is used in.Now we are going to turn our focus to generating properties for each of the settings in the configuration file. For this we’ll be adding more code to the class block to open and read the settings from the project’s configuration file (which we’ll have to write code to retrieve). We’ll also have to decide how to convert string values to typed equivalents.

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

Generating Property Code

Before we read the configuration file let’s go ahead and modify the template to generate the properties that back the settings. We can do this by creating a simple method that returns a pre-defined list of setting key and value pairs. We can then enumerate them and generate the corresponding properties. Since this is template generation code we’ll place it in the class block where the other methods are. We’ll also make it private as it is specific to this template. Later when we generalize the template private methods will remain with the template.

private Dictionary<string, string> GetAppSettings ()
{
   //Stub some values
   return new Dictionary<string, string>()
   { 
      { "IntValue", "10" },
      { "StringValue", "Hello" },
      { "DoubleValue", "45.6" }
   };
}

Now we can remove the static public properties we initially added and replace them with code that enumerates each setting and generates the corresponding public property. Notice that we mix code blocks with static content. This is handy to do (reminds me of ASP.NET or MVC) but can get confusing if used too much. The setting key becomes the property name and is used to retrieve the value at runtime. The setting key is inserted using an expression block.

<# 
foreach ( KeyValuePair<string, string> setting in GetAppSettings()) 
{
#>
   public string <#= MakeIdentifier(setting.Key) #>
   { 
      get { return GetConfigSetting("<#= setting.Key #>"); 
   }
<# } 
#>

Saving the template should cause the three properties to be generated. At this point we could reference the properties in some code, compile and run the code and the values from the config file should be pulled used. We’re close to being done with the basic implementation but let’s clean up a few things before we start reading values from the config file. The first thing to note is that GetSafePascalName has been renamed to MakeIdentifier. It is shorter and more clear.

Determining the Property Type

We need to determine the correct property type. If the setting is an integer, for example, we’ll generate an Int32 type. For most settings the following types should work: int, double, bool and string. We’ll use simple test parsing to find the correct type. If we cannot match any type we’ll use string. The GetSettingType method accepts the setting the value and attempts to parse it. The Type is returned back so we can use it to generate the correct code later.

private Type GetSettingType ( string value )
{
   //Try to convert to int first
   int ivValue;
   if (Int32.TryParse(value, NumberStyles.Integer, null, out iValue))
      return typeof(int);

   //Double is next
   double dValue;
   if (Double.TryParse(value, NumberStyles.Float | NumberStyles.AllowCurrencySymbol, null, out dValue))
      return typeof(double);

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

   return typeof(string);
}
<# 
foreach (KeyValuePair setting in GetAppSettings())
{
   var type = GetSettingType(setting.Value);
#>

   public <#= GetFriendlyName(type) #> <#= MakeIdentifier(setting.Key) #>   
   {
      get { return GetConfigSetting("<#= setting.Key >); }
   }
<# }
#>

I like for my code to use the standard aliases for built in types so I wrote a quick and dirty function to get the alias, if any, given a type. It is not efficient but it works without me having to write a mapping table myself. We need to include the System.CodeDom, System.Globalization and Microsoft.CSharp namespaces before this will compile.

public static string GetFriendlyName ( Type source ) { var provider = new CSharpCodeProvider(); var tr = new CodeTypeReference(source); var str = provider.GetTypeOutput(tr); //Ref and out parameters have an & on the end so strip it off if (str.EndsWith(“&”)) return str.Substring(0, str.Length – 1); return str; }

Type conversion

The properties now have the correct type but we’re returning strings from the properties so the code won’t compile. We now need to convert the string values as obtained from ConfigurationManager to the correct type. To keep this relatively clean we’ll create yet another private method that accepts a type as a parameter and returns the name of the conversion function to call. We’ll wrap the entire call to ConfigurationManager with a call to the conversion routine. In the case of a string there is no conversion so the generated property will just be wrapped in parenthesis.

private Lazy<Dictionary<Type, string>> m_settingsConverter = new Lazy<Dictionary<Type, string>>(InitializeSettingsConverter);

private string GetSettingConverter ( Type type )
{
   string converter;
   if (m_settingsConverter.Value.TryGetValue(type, out converter))
      return converter;

   return "";
}

private static Dictionary<Type, string> InitializeSettingsConverter ()
{
   return new Dictionary<Type, string>() {
      { typeof(sbyte), "SByte.Parse" },
      { typeof(short), "Int16.Parse" },
      { typeof(int), "Int32.Parse" },
      { typeof(long), "Int64.Parse" },

      { typeof(byte), "Byte.Parse" },
      { typeof(ushort), "UInt16.Parse" },
      { typeof(uint), "UInt32.Parse" },
      { typeof(ulong), "UInt64.Parse" },

      { typeof(float), "Float.Parse" },
      { typeof(double), "Double.Parse" },
      { typeof(decimal), "Decimal.Parse" },

      { typeof(bool), "Boolean.Parse" },
      { typeof(char), "Char.Parse" },
      { typeof(DateTime), "DateTime.Parse" },
      { typeof(Guid), "Guid.Parse" },
      { typeof(TimeSpan), "TimeSpan.Parse" },

      { typeof(string), "" }
   };
}

The type to converter mapping is stored in a dictionary to optimize performance. For this code I’m using the standard Parse methods but you can replace it with any method you want. Here’s the updated property block.

public <#= GetFriendlyName(type) #> <#= MakeIdentifier(setting.Key) #>
{
   get { return <#= GetSettingConverter(type) #>(GetConfigSetting("<#= setting.Key #>")); } 
}

We’ve finished the propery generation code. Depending upon the value stored in the config file (when the template is generated) a strongly typed property will be generated for each app setting. Type conversion is used to convert the value in the config file at runtime to the correct type. We have effectively replaced the Settings-based code added to .NET with our own implementation that works similarily but without having to add anything to our config files. But we aren’t quite done yet as we’re still using hard coded values. It is time to retrieve the settings from the config file directly.

Reading the Configuration File

In order to get the settings from the configuration file we have to locate the config file in the project (keeping in mind it could be a web or Windows app). We already have the code to find a specific project item so we just need to use it to find the config file. For now we’ll assume the config file exists. In a later article we’ll see how to handle a missing config file.

public EnvDTE.ProjectItem FindConfigProjectItem ( EnvDTE.Project source )
{
   return FindItem(source, "web.config", true) ?? FindItem(source, "app.config", true);
}

Now it becomes a simple matter of loading the settings from the config file using ConfigurationManager. We’ll modify our stub method of earlier to return the information. Note we need to add another import for System.Configuration. We’ll also need to ensure we have an assembly directive for System.Configuration because it is not included by default.

private KeyValueConfigurationCollection GetAppSettings ()
{
   //Get the config file
   var configItem = FindConfigProjectItem(ActiveProject);

   return GetAppSettings(configItem);
}

private KeyValueConfigurationCollection GetAppSettings ( EnvDTE.ProjectItem configItem )
{
   if (configItem != null)
   {
      var configFile = new ExeConfigurationFileMap() { ExeConfigFilename = GetProjectItemFileName(configItem) };
      var config = ConfigurationManager.OpenMappedExeConfiguration(configFile, ConfigurationUserLevel.None);

      return (config.AppSettings != null) ? config.AppSettings.Settings : null;
   };

   return null;
}

Notice that we changed the return type to match what the configuration subsystem will return. We’ll have to update the foreach block as well but the rest of the code is fine.

<# 
foreach (KeyValueConfigurationElement setting in GetAppSettings()) 
{
   var type = GetSettingType(setting.Value);
#>

Save the template and the same properties we had hard coded earlier should still be available. This time though they are being pulled from the project’s config file. Feel free to add more settings to confirm everything is working correctly. At this point we have a fully working and useful template that we can use in projects to clean up settings code. Not bad for 300 lines of code. We have met 3 of the 6 initial requirements. In the next article we’re going to modify the template to meet the remaining requirements making it more useful and flexible to meet the needs of complex projects.

Download the Code

Comments

  1. What i do not realize is actually how you are not actually much more well-liked than you might be now. You’re very intelligent. You realize therefore significantly relating to this subject, made me personally consider it from numerous varied angles. Its like women and men aren’t fascinated unless it’s one thing to accomplish with Lady gaga! Your own stuffs excellent. Always maintain it up!

  2. I like the helpful info you supply on your articles. I will bookmark your blog and check once more right here frequently. I’m moderately sure I will be told many new stuff proper right here! Best of luck for the next!

  3. Greetings! I’ve been following your weblog for some time now and finally got the bravery to go ahead and give you a shout out from Houston Texas! Just wanted to mention keep up the good work!

  4. Thank you for sharing your info. I truly appreciate your
    efforts and I am waiting for your further write ups thanks once
    again.