Using T4 to Create an AppSettings Wrapper, Part 2

Published on: Author: Michael

In the first article in this series we created a basic, static T4 template.  The template allowed us to replace standard boilerplate code for reading an app setting


string setting = ConfigurationManager.AppSettings[“someSetting”];

with strongly typed property references like this


var myIntSetting = AppSettings.Default.IntValue;
var myDoubleSettig = AppSettings.Default.DoubleValue;

Here’s a summary of the requirements from the first article (slightly reordered).


  1. All settings defined in the project’s config file should be exposed, by default, as a public property that can be read. I’m not interested in writing to them.
  2. Based upon the default value (in the config file) each setting should be strongly typed (int, double, bool, string).
  3. The configuration file cannot be cluttered with setting-generation stuff. This was the whole issue with the Settings designer in .NET.
  4. Sometimes the project containing the config file is different than the project where the settings are needed (ex. WCF service hosts) so it should be possible to reference a config file in another project.
  5. Some settings are used by the infrastructure (such as ASP.NET) so they should be excluded.
  6. Some settings may need to be of a specific type that would be difficult to specify in the value (ex. a long instead of an int).

In this article we’re going to convert the static template to a dynamic template and begin working on requirements 1-3.  From last time here are the areas of the template that need to be made more dynamic.


  1. The namespace of the type
  2. The name of the type, including pretty formatting
  3. Each public property name and type

Template Structure


Pretty much every T4 template will include a type within a namespace.  A template should follow the standard practices that are currently in use in terms of names and formatting.  Every (code) project has a default namespace.  It is expected that all types added to the project be placed into the default namespace.  However, at least in C#, if a file is placed into a subfolder then the subfolder becomes part of the namespace.  The template should use the default namespace combined with the folder(s) containing the file. Unfortunately this information is not directly accessible to the template.  We’ll have to write some code to get this information but a quick overview of underpinnings of the template is in order.


A template is compiled into a derived type of TextTransformation.  This type exposes the core functionality needed for a template include error reporting, writing to the generated file and context information.  Unfortunately the base type does not expose the information we need so we’ll have to call out to Visual Studio (the host).  In order to do that we first need to set the hostspecific attribute on the template directive to true.  You will probably want to set the debug attribute to true as well to make debugging easier.  Once the host attribute is set we can use the Host property to access information about the template.  This opens up most of the dynamic data we need.


It is important to distinguish between code used for template generation vs code that will end up in the generated file.  Template generation code will reside inside statement blocks (<# #>), expression blocks (<#= #>) or class blocks (<#+ #>).  A statement block is used to execute code during template generation.  Any variables defined in block are accessible to later blocks.  An expression block is used to inject a value into the generated code.  It is normally a: variable defined in a statement block, a function defined in a class block or a property of the template class.  Expression blocks are how we’ll get the dynamic data into the generated code. A class block is used to add members to the generated template class.  Class blocks are useful for creating reusable functions that can be used during template generation.  One limitation of class blocks is that they cannot be followed by statement blocks.  In general this isn’t an issue because a statement block is used to start the code generation and therefore it will be first.


Standard Variant Replacement


Now that we have access to Host we can get the name of the template file (as set in the Add New Item dialog).  The file name (without the extension) will be the name of the settings class so we should go ahead and store off the name in a variable for later use.


<#
    var ClassName = Path.GetFileNameWithoutExtension(Host.TemplateFile);
#>

We’re using Path here so we should also include an import for System.IO since we’ll be needing this namespace later.  Now that we have a variable representing the class name we should go ahead and replace the static AppSettings typename with the type name as determined by the filename using an expression block.


internal partial class <#= ClassName #>

Each time you make a change to the template you should go ahead and save the template.  Check the Error List window to ensure the template compiled correctly and verify the generated file.  Finding template generation errors can be difficult so verifying each change as you go along makes things much easier.  Note that we are currently ignoring the case where the name might contain invalid identifier characters.


Getting the namespace requires a little more work.  There are quite a few ways to get the information but I prefer to get the namespace straight from the project.  Each project item has a custom namespace property but I generally don’t bother using it.  Once you start needing information from VS then you’re going to run into DTE which is the main VS object.  A good understanding of the VS object model will really be beneficial but is beyond the scope of this post so I’m just going to provide the necessary code to get the project associated with the template.  Because this is useful code and we’ll need it later I’m going to place it in a class block at the bottom of the template.  Remember these methods will be part of the type that is generated to back the template generation.


  1. Add assembly directives to include the required Visual Studio assemblies (EnvDTE in this case).
  2. Add import directives to include the DTE namespaces.  Note that I prefer to not import the EnvDTE namespace where the core model is at because it makes it much cleaner to read, in my opinion.
  3. Add the code to a class block at the bottom of the template.  Note there are several helper methods that will be beneficial later.

<#+
public
 EnvDTE.Project ActiveProject
{
    get
    {   if (m_activeProject == null)
        {
            if (DteInstance != null)
            {
                var projects = (Array)DteInstance.ActiveSolutionProjects;

                m_activeProject = (projects != null && projects.Length > 0) ? (EnvDTE.Project)projects.GetValue(0) : null;
            };
        };

        return m_activeProject;
    }
}

public EnvDTE.DTE DteInstance
{
    get
    {
        if (m_dte == null)
            m_dte = (EnvDTE.DTE)((IServiceProvider)Host).GetService(typeof(EnvDTE.DTE));

        return m_dte;
    }
}

public static EnvDTE.ProjectItem FindItem ( EnvDTE.Project source, string itemName, bool recurse )
{
    var items = source.ProjectItems;

    //ProjectItems.Item() will throw if the item does not exist so do it the hard way        
    foreach (EnvDTE.ProjectItem child in items)
    {
        if (String.Compare(child.Name, itemName, true) == 0)
            return child;
    };

    if (recurse)
    {
        foreach (EnvDTE.ProjectItem child in items)
        {
            var item = FindItem(child, itemName, true);
            if (item != null)
                return item;
        };
    };

    return null;
}

public static EnvDTE.ProjectItem FindItem ( EnvDTE.ProjectItem source, string itemName, bool recurse )
{
    var items = source.ProjectItems;

    //ProjectItems.Item() will throw if the item does not exist so do it the hard way        
    foreach (EnvDTE.ProjectItem child in items)
    {
        if (String.Compare(child.Name, itemName, true) == 0)
            return child;
    };

    if (recurse)
    {
        foreach (EnvDTE.ProjectItem child in items)
        {
            var item = FindItem(child, itemName, true);
            if (item != null)
                return item;
        };
    };

    return null;
}

public string GetNamespaceForTemplate ()
{
    //Get the project’s root namespace
    var rootNamespace = ActiveProject.Properties.Item(“RootNamespace”).Value as string;

    //Get the project item for the template file
    var templateItem = FindItem(ActiveProject, Path.GetFileName(Host.TemplateFile), true);

    //Walk backwards until we get to the project root, concatenate each folder to the namespace
    var parentNamespaces = new List<string>();
    var parent = templateItem.Collection.Parent as EnvDTE.ProjectItem;
    while (parent != null)
    {
        parentNamespaces.Add(GetSafePascalName(parent.Name));
        parent = parent.Collection.Parent as EnvDTE.ProjectItem;
    };

    var orderedNamespaces = parentNamespaces as IEnumerable<string>;
    var folderPath = String.Join(“.”, orderedNamespaces.Reverse());

    return (folderPath.Length > 0) ? rootNamespace + “.” + folderPath : rootNamespace;
}

public string GetProjectItemFileName ( EnvDTE.ProjectItem item )
{
    return (item != null && item.FileCount > 0) ? item.FileNames[0] : “”;
}

public string GetSafePascalName ( string baseName )
{
    var builder = new System.Text.StringBuilder();

    //Starts with capital letter or underscore
    bool isFirstLetter = true;
    foreach (var ch in baseName)
    {
        if (isFirstLetter)
        {
            if (Char.IsLetter(ch))
                builder.Append(Char.ToUpper(ch));
            else if (Char.IsDigit(ch) || ch == ‘_’)
                builder.Append(ch);

            isFirstLetter = false;
        } else
        {
            if (Char.IsLetterOrDigit(ch) || ch == ‘_’)
                builder.Append(ch);
        };
    };

    if (builder.Length == 0)
        builder.Append(‘_’);

    return builder.ToString();
}

private EnvDTE.DTE m_dte;
private EnvDTE.Project m_activeProject;
#>

The GetSafePascalName method is used to generate a safe Pascal name.  It is probably a good idea to go back and add a call to this method when setting the ClassName variable from earlier.


<#
   var ClassName = GetSafePascalName(Path.GetFileNameWithoutExtension(Host.TemplateFile));
#>

Finally we can use the new functionality to replace the static namespace.


namespace <#= GetNamespaceForTemplate() #>
{  …

At this point we should be able to move the template file to a different folder and the generated code should update the namespace accordingly.  If we rename the template file then the generated type name should reflect this as well.


We’ve written a lot of code and we’re not done yet.  But the functionality that has been added will be reusable in all the other templates we create.  In the next article in this series we’ll replace the static properties with the real settings from the configuration file.

34 Responses to Using T4 to Create an AppSettings Wrapper, Part 2 Comments (RSS) Comments (RSS)

  1. An outstanding share! I’ve just forwarded this onto a co-worker who was doing a little research on this. And he actually ordered me breakfast due to the fact that I discovered it for him… lol. So allow me to reword this…. Thank YOU for the meal!! But yeah, thanx for spending the time to discuss this subject here on your web page.

  2. I was very happy to uncover this web site.

    I want to to thank you for ones time for this particularly wonderful read!

    ! I definitely liked every part of it and i
    also have you saved to fav to look at new information on your web site.