Using T4 to Create an AppSettings Wrapper, Part 5
In the last article we broke out the template into 2 separate template files: main and nested. A developer adds the main template to their project but it includes the nested template where the actual work is done. In this article we’re going to update the included template to allow a developer to alter the generated code without having to touch the nested template.
In the original article we listed 6 requirements for the template. At this point we have met half of them. By adding customization we will meet the final three. They are:
- Exclude some settings
- Change the type of a setting
- Use the configuration file of a different project
Update 28 Aug 2016: Refer to this link for updated code supporting Visual Studio 2015.
Customizing a Template
Customizing a template is actually not that hard now that it’s broken up. The key is in understanding what we can and cannot change when we release updates. The main template can be updated but in order to get the updates a developer would need to remove the main template from their project and add it back. The nested template can be updated as needed. Provided the developer reruns the template they will get any changes.
To customize a template we add public members (properties or methods) to the template class in the nested template. As with a normal class we can then use the properties/methods when it comes time to execute the template. We can even add new customizations in future updates without breaking the developer’s existing customizations.
In the main template a developer can call the public members after the template class instance is created but before it renders. Since they will not have access to Intellisense it makes sense to provide some commented code demonstrating the available customizations. Additionally we can provide some basic customizations if desired. Since the main template is never updated once it is added to the project (unless the developer does it explicitly) the customizations are preserved even if the nested template is updated. Now let’s focus on adding each of the customizations we listed earlier.
Excluding Settings
With certain projects we often find extra settings that we do not care about included in the appSettings section. ASP.NET is a good example because it stores several options there. We probably do not want to generate code for these. For this customization we will allow the developer to exclude a setting based upon its name.
In the nested template add a public method called ExcludeSetting
that accepts the name of a setting. Some frameworks have a lot of settings so it would be painful to exclude each one. Therefore we’ll make the exclusion functionality a little smarter by allowing an asterisk on the end (i.e. webpages*). The asterisk will be a wildcard for anything that follows the name when doing matching. To simplify the code define 2 private lists to store the excluded settings (one for regular and one for wildcard exclusions). Also define a private method called IsExcluded
that takes a setting name and returns whether it should be excluded or not based upon the lists.
public AppSettingsTemplate ExcludeSetting ( string settingName ) { if (settingName.Contains("*")) { var token = settingName.Substring(0, settingName.IndexOf('*')); m_exclusionMasks.Add(token); } else m_exclusions.Add(settingName); return this; } private List<string> m_exclusions = new List<string>(); private List<string> m_exclusionMasks = new List<string>(); private bool IsExcluded ( string settingName ) { return m_exclusions.Where(x => String.Compare(x, settingName, true) == 0).Any() || m_exclusionMasks.Where(x => settingName.StartsWith(x, StringComparison.OrdinalIgnoreCase)).Any(); }
In the code where the settings are enumerated call the IsExcluded
method to determine if the setting should be generated or not.
<#+ foreach (KeyValueConfigurationElement setting in GetAppSettings()) { if (IsExcluded(setting.Key)) continue; var type = GetSettingType(setting.Value); #>
Now we can add some default entries for some commonly used frameworks. Notice that we can chain calls together because the public method returns the template instance.
<# var template = new AppSettingsTemplate(); // Exclude any settings that are not needed, use * for a wildcard template.ExcludeSetting("aspnet:*") .ExcludeSetting("webpages:*); template.Render(); #>
Changing a Setting’s Type
By default the template will determine the type of a setting based upon its value. But sometimes the value doesn’t properly indicate the type (i.e. 123 for a phone extension) or a specific type is needed (i.e. a long even if the value is 123). For this we will allow the developer to specify the type to use for a setting rather than relying on the default behavior.
Add a public method to the nested template called OverrideSettingType
. It should accept a setting name and a type. Store the setting name in a private dictionary for later.
public AppSettingsTemplate OverrideSettingType ( string settingName, Type settingType ) { m_settingsTypes[settingName] = settingType; return this; } private Dictionary<string, Type> m_settingsTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
Modify the GetSettingType
method to check the override dictionary before trying to determine the type of the setting based upon its value. Note that the setting name needs to be added to the parameter list so this check can be done. Update the calling code accordingly.
private Type GetSettingType ( string name, string value ) { Type explicitType; if (m_settingsTypes.TryGetValue(name, out explicitType) && explicitType != null) return explicitType; //Use heuristics }
Here is how it might look in the main template.
//Override a setting's type template.OverrideSettingType("IntValue", typeof(long));
Using a Different Project’s Configuration
Normally you would agree that a project should only rely on its own settings but there are a few cases where this doesn’t make sense. One example is a WCF service host. The host generally consists of only the configuration file for the service and the .svc file. The actual implementation is stored in a separate project so the service can be re-hosted with ease. In this case we would want to add the template to the implementation project but have it rely on the host project’s configuration file. For this customization we will allow the developer to specify a project other than the active project from which to read the configuration file.
Add a new public property to the nested template called ConfigurationProject
. If it is set then it is the name of the project where the config file resides that will be read.
public string ConfigurationProject { get; set; }
The hard part is changing the nested template to use a different project. Fortunately we already have almost all the code. In an earlier article we wrote a function to get the settings given a ProjectItem
. Up until now we’ve been using ActiveProject
. To use a different project we need only find the project in the solution and then pass it to FindConfigProjectItem
instead.
private KeyValueConfigurationCollection GetAppSettings () { var project = ActiveProject; //If a custom configuration file is specified then find the project if (!String.IsNullOrEmpty(ConfigurationProject)) project = FindProject(ConfigurationProject); //Get the config file var configItem = FindConfigProjectItem(project); return GetAppSettings(configItem); } public EnvDTE.Project FindProject ( string projectName ) { if (String.IsNullOrEmpty(projectName)) return null; foreach (EnvDTE.Project project in DteInstance.Solution.Projects) { if (String.Compare(project.Name, projectName, true) == 0) return project; }; return null; }
Here is how a developer would specify it in the main template.
//Change the project to use template.ConfigurationProject = "";
Validation
Up until now we haven’t really needed any validation. But as we allow developers to customize the template it makes sense to ensure the template can be generated. If something goes wrong the template host generally spews out useless messages.; Before we finish up lets add some basic validation to the template. With T4 you can generate either warnings or errors. With the T4 Toolbox these can be generated using the Warning
and Error
methods.
For this template the following validation seems appropriate.
- If a custom configuration project is set then the project must exist otherwise it is an error.
- If no config file can be found then this would be a warning.
- If the config file has no app settings then this would be a warning.
I personally like to do validation before the template runs so we’ll add a Validate
method that wraps the validation logic. T4 Toolbox already defines a base validation method so we can override it. As an optimization we will store off the found settings so we do not have to search for them again when we render.
Add a Validate
method to the nested template that handles all the validation rules.
private KeyValueConfigurationCollection AppSettingsInfo { get; set; } protected void Validate () { base.Validate(); KeyValueConfigurationCollection info = null; var project = String.IsNullOrEmpty(ConfigurationProject) ? ActiveProject : FindProject(ConfigurationProject); if (project != null) { var configItem = FindConfigProjectItem(project); if (configItem != null) { info = GetAppSettings(configItem); if (info == null || info.Count == 0) Warning("No appSetting entries found."); } else Warning("Unable to locate configuration file."); } else Error("Unable to locate configuration project '{0}'.", ConfigurationProject); AppSettingsInfo = info ?? new KeyValueConfigurationCollection(); }
The original GetAppSettings
method can go away now and inside the template text we can replace the call to it with AppSettingsInfo
. When the template runs the validation method will execute and we’ll get pretty errors and warnings if appropriate.
Next Steps
At this point we have a template that a developer can add to their project and customize to meet their needs. The template relies on a nested template to do the actual heavy lifting of generating the final file. There are some downsides to this approach including the fact that the project needs to include multiple files even though only 1 is added to the project. Another is the fact that even though we have nested the core functionality we would still need to replicate the code if we wanted to create a different template. Even worse is the fact that if we update the core template (add enhancements or fix bugs) then every project that relies on the template would need to get an updated version. In the next article we’ll package the template up into a reusable component that is easy to deploy and update and will have minimal impact on the end developer.
Download the Code
Good article. I am dealing with a few of these issues as well.
.
Thanks for sharing your info. I truly appreciate your efforts and I will
be waiting for your next write ups thanks once again.