February 2013 - Posts

Using T4 to Create an AppSettings Wrapper, Part 1

AppSettings are settings stored in your configuration file under the <appSettings> element. Almost every application has them. Each setting consists of a name and value. To access such a setting in code you need only do this.

string setting = ConfigurationManager.AppSettings["someSetting"];

There are a couple of problems with this approach.

  • Quite a bit of boilerplate code to access a setting given what it is actually doing
  • The setting name is hard coded and must match the config file
  • The returned value is a string so if you need a different type then you'll need to convert it

In .NET v2.0 Microsoft added the Settings class to work around these issues. It allows you to create a setting with a type and value and the designer will generate a type to back it where each property matches the setting. This seems great but never really took off. Not even Microsoft uses it in their own framework. Part of the problem is that the config entries it generates are overly complex storing things like type information, default values and other things. Needless to say appSettings continue to be popular anyway. Fortunately we can get the simplicity of appSettings with the power of the newer Settings class all via T4.

In this series of posts I'm going to walk through the process of generating such a template including the ability to add some more advanced functionality.  A full discussion of T4 is beyond the scope of a blog so refer to the following links for more information.

Requirements

At my company I was tired of the limitations of working with appSettings so I combined some work I've done in the past with the T4 engine to produce a template that all projects can use to simplify working with app settings. Because of the dynamics of the code I work in I had some additional requirements.

  • 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.
  • Based upon the default value (in the config file) each setting should be strongly typed (int, double, bool, string).
  • 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.
  • Some settings are used by the infrastructure (such as ASP.NET) so they should be excluded.
  • 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).
  • The configuration file cannot be cluttered with setting-generation stuff. This was the whole issue with the Settings designer in .NET.

Defining the Generated Code

Before you can write a T4 template you need to know what you are going to generate.  The easiest way to do that is to write the actual code you want, given specific inputs.  Here is the basic code to be generated given the specified inputs.

<appSettings>
        <add key="DoubleValuevalue="45.678" />
        <add key="IntValuevalue="123" />
        <add key="StringValuevalue="Text" />
    </appSettings>
using System;
using System.Configuration;

namespace P3Net
{        
    internal partial class AppSettings
    {    
        /// <summary>Gets the default instance.</summary>
        public static AppSettings Default
        {
            get { return s_defaultInstance; }
            protected set { s_defaultInstance = value ?? new AppSettings(); }
        }

        #region Setting Properties

        public double DoubleValue
        {
            get { return Convert.ToDouble(GetConfigSetting("DoubleValue")); }
        }

        public int IntValue
        {
            get { return Convert.ToInt32(GetConfigSetting("IntValue")); }
        }

        public string StringValue
        {
            get { return Convert.ToString(GetConfigSetting("StringValue")); }
        }
        #endregion

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

        private static AppSettings s_defaultInstance = new AppSettings();
    }
}

To make the generated type easier to use it is marked as partial and the main method (GetConfigSetting) that all settings are read through is virtual.  But to keep calling code simple the type is implemented as a singleton (although it isn't enforced).  Each property is backed by the corresponding entry from the config file.  For our purposes type conversion will be handled by the Convert class but you should really use a more resilient type conversion system.   

Identifying the Variant Parts

Once you've defined the format of the final code you need to identify the parts that will vary based upon the inputs.  For the earlier code the properties will vary based upon the config file.  There will be a property for each setting in the config.  The property name will follow the setting name and the property type will be determined by the value stored in the config file.  Because settings are returned as strings by the subsystem there will be a type conversion call as well.

But there is more variant components than just the inputs and these must be taken into account as well.  When you add a type to a project (such as the generated code) it is expected to place the type in the default namespace for the project with any child folder taken into account.  Therefore the namespace name is a variant in the template.  The actual name of the type should generally follow the name of the file.  The filename is set by the user when they add the template to the project.  Therefore the type name needs to be a variant and any references to it must be variants as well. 

In the above code all the variant parts are highlighted in yellow.  Each of the highlight parts will need to be converted to an expression that T4 can replace when it generates the template.

Creating the Basic T4 Template

Now that we know what code we want to generate it is time to create the template.

  1. Create a new project or opening an existing project where the settings class will be used.
  2. Ensure the project has a configuration file with some app settings defined.  (I assume you are using the entries mentioned above).
  3. Add a new Text Template item (under Visual C#\General) to the project called AppSettings.tt.  This will be the template name. 
  4. Inside the .tt file paste the actual code you want generated at the bottom of the file.
  5. Save the file.  Whenever the .tt file is saved it will rerun the template.
  6. Assuming nothing went wrong you can expand the AppSettings.txt file under the template and you should see the correct generated code.

If the template failed for any reason then the errors will appear in the Error List.  Otherwise you should be able to set up a simple test call to verify the code is working. 

var intValue = AppSettings.Default.IntValue;
var doubleValue = AppSettings.Default.DoubleValue;
var stringValue = AppSettings.Default.StringValue;

But if you try to do so you'll find that the type doesn't show up.  That is because, by default, T4 templates generate text files and not code files.  The output directive in the T4 file determines what type of file to generate.  Change it from ".txt" to ".generated.cs" and save the template.  The type should now show up, provided the project name matches the namespace used in the template.  If not then add a using/import statement.

Parts of a T4 Template

Let's take a look at the template file.  T4 directives appear between <#@ #> delimiters.  Directives generally appear at the top of the file. 

  • template specifies that this is a T4 template.  This directive can appear only once.

    The debug attribute should be set to true to enable debugging until the template is working.  The hostspecific attribute specifies whether the template needs access to the host.  We'll talk about this later.  The language attribute specifies the language of the template code.
  • assembly is equivalent to adding a reference to a project in that it indicates an assembly that is needed by the template.  Any number of these directives can appear.  Note that this is for the generation of the template itself and not the final generated code.  Only assemblies needed to generate the template code should be listed.  The assembly must be accessible by T4.
  • import is equivalent to a using/import statement.  Any number of these can appear.  As with the assembly directive this only impacts the template generation.  Any types needed to generate the template need to have their namespace(s) imported.  The final generated code will explicitly include the using/import statements that it will need.
  • output controls the output of the template.  As mentioned earlier the extension attribute specifies the extension added to the generated file.  It is appended to the template name to generate the final file name.  For source files you should use the standard practice of using ".generated.cs".  Designers use the default ".designer.cs".  When the file is generated, if it is not yet part of the project, then it gets added automatically.  VS will use the file extension to determine how to properly add the file to the project (ex. content, compile).

After the directives is the code that is generated.  An important note about the T4 generator, blank lines matter.  Some directives are stripped out of the generated code, such as the <#@ @> directives but other directives are replaced with a blank line.  Additionally any blank lines that appear in the template are also written verbatim.  Normally when writing code we try to format the code neatly.  In the case of T4 put your formatting rules on the bench.  Place the text to be generated right after the directives if you don't want blank lines in the generated file.  It is also generally a good idea to place a file header in the generated code that mentions the file is auto generated and any changes will be lost.

At this point we have a working T4 template but it doesn't do anything fancy.  It is nothing more than a glorified code snippet but we have set everything up to easily now convert it to a more dynamic template.  We'll cover that in the next part of this series.

Attachment: T4_Part1.zip
Posted Mon, Feb 18 2013 by Michael Taylor | 3 comment(s)
Filed under:
MVP of the Year and Connect

I recently found out that I was one of the MVPs of the year for our group.  Wow what an honor!!  Given the caliber of MVPs one cannot help but be humbled by this.  Unfortunately other obligations prevent me from attending the ceremony to receive the award.  I found out later that one of the reasons was that I was supposedly the top bug reporter on VS 2012 for US dev MVPs.  I reported 27 issues and 13 were actually resolved.  This got me to thinking about Connect and how Microsoft has historically used it.

Historically when an issue was reported someone at Microsoft would try to replicate the issue.  If they could then they would escalate it to the team and you'd receive some feedback.  At that point the issue would either be fixed or, more likely, closed without reason.  More recently Microsoft has started to close items with short descriptions like 'by design' or 'won't fix'.  The one that drives me mad though is 'we have to evaluate the priority of each item reported against our schedule and this issue is not sufficiently important.  We will review it in the future'.  Closed.  The problem is that I'm not convinced they every do "review it in the future".  Even worse is that once an item is closed you cannot do anything with it anymore. 

If, as happened recently to me, the folks at MS failed to grasp the issue you were reporting and closed the item then there is no way to tell them they messed up.  Recently I reported an issue to Microsoft about the behavior of inline tasks for MSBuild (https://connect.microsoft.com/VisualStudio/feedback/details/768289/msbuild-path-used-for-inline-task-reference-is-not-honored).  The issue is that an inline task can reference an assembly using a path.  At compile time this is fine but at runtime the assembly path is not honored so unless you copy the assembly to the same location as the task then it will fail to the find the assembly.  Now I understand the reasoning behind why it would fail.  I also know that I'm not the only one who has seen this issue.  It has been reported on multiple blogs over the years.

Somehow the folks looking at the issue got caught up with what the sample code was trying to do rather than what the actual bug was and reported that I should be using some built in task instead.  How does that in any way relate to my problem?  I don't know but the item was closed anyway.  Without starting a new item I cannot recover this closed item.  Sure I left a follow up comment about it but the item is still closed, the bug still exists and I doubt it will ever get resolved.  And I'd like to think that as a contributor to the community that my items get a least a little more attention than the general user but it wouldn't appear so in Connect.  If MS really wants us to report bugs and Connect is the tool to do so then the entire system needs to evolve into a more full featured issue tracking system where we can alert MS to issues that may have been closed incorrectly.  Even more important issues that "may be resolved in a future release" shouldn't be closed but deferred so we know that at least they are under consideration.  Right now Closed means it's fixed, it's by design, it cannot be repro or it ain't going to be fixed.

Historically MS has taken some flax from the community about the uselessness of Connect.  With VS2012 they seem to have upped their game and started taking feedback more seriously but there is still much work to be done.  There has to be more insight into where an item is in the process, policies for getting items reevaluated and a better system for identifying items that are closed or deferred.  Perhaps the User Voice site will take over bugs as well.  Right now it is more for suggestions.  Time will tell.  Having 50% of my reported items resolved indicates that Connect is starting to work, at least for me, but it has to work for everybody or else it isn't going to be used.

Our industry is plagued by large egos.  I try to keep mine in check (except around a few people who I know will take me for who I am, not what I've done).  Where I work we have a motto "If you're truly good you don't have to say anything".  What that means is that bragging about being an MVP, writing a book, publishing a popular framework or whatever else gets you nowhere.  If you're truly good your works will speak for themselves.  As such I will quietly place my plaque next to my MVP awards and move on.  But recently one of our team members won both the Chili Cookoff contest and the Employee of the Year award in the span of two weeks.  They proudly carried their awards the next few days to all their meetings.  Maybe, just maybe, I'll carry mine to a couple of meetings.  If you're truly good you don't have to say anything but awards don't talk do they :}