By: Leblanc Meneses
MSBuild FlexibleConfigTask
A couple of weeks ago I worked on a few MSBuild tasks to improve my team’s workflow. As features are added or improved, files like Web.config, App.config, ServiceReferences.ClientConfig, and even non xml based configuration files need to be merged and updated. This task provides flexibility by conditionally merging a file. Conditions can be any valid CSharp boolean expression. In practice, this can be used to merge settings specific to a user (distributed workforce not sharing a common infrastructure), or an environment (integration/QA/production).
Leveraging the task
<Target Name="Sync"> <ItemGroup> <WwwParameters Include="MSBuildStartupDirectory"> <Key>MSBuildStartupDirectory</Key> <Value>$(MSBuildStartupDirectory)\MSBuild</Value> </WwwParameters> <WwwParameters Include="IsAdminWww"> <Key>IsAdminWww</Key> <Value>false</Value> </WwwParameters> </ItemGroup> <Message Text="Creating Www.Web.config" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(WwwParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Www.Web.config" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\Www.Web.config" /> </Target>
Line 14, contains our custom MSBuild task that takes two inputs and provides two output:
- Parameters: (input) is a dictionary that can be used inside the BaseLineFile for specific task setting replacements.
- BaseLineFile: (input) is the input file that is conditionally evaluated per user/platform.
- ErrorLogFile: (output) is the output location of the error log file incase the task fails.
- OutputFile: (output) is the output location of the file that needs to be regenerated.
Example
App.debug.config is the data file we use in our unit test. Although not a real example, it shows the features that are supported by this task.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<?flexibleconfig
if( "HOME" == Environment.GetEnvironmentVariable("WORKLOCATION", EnvironmentVariableTarget.Process) )
{
writer.AppendLine("testing");
}
else
{
?>
<flexibleconfig path="$(MSBuildStartupDirectory)\ParserData\appSettings.config" />
<?flexibleconfig
}
?>
</configuration>
App.debug.config is the baseline file that should be fed into FlexibleConfigTask.
The section enclosed in flexibleconfig preprocessor tag will be evaluated using the C# compiler. All other sections will be pushed to the standard output. To write to standard output from the C# evaluated section you can use "writer" directly. To include files you can use System.IO.File.ReadAllText from within the C# section or directly use the include notation as shown in line 13. Here is the file appSettings.config that is included by App.debug.config.
<appSettings> <add key="key1" value="value1" /> </appSettings>
After FlexibleConfigTask has executed the output generated is shown below. (Expected0001.config)
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="key1" value="value1" /> </appSettings> </configuration>
Since this test did not set environment variable it does not evaluate line 5 and "testing" will not appear in the output file.
Notice that line 13 uses the passed in parameter from the MSBuild task: $(MSBuildStartupDirectory).
Integration Tests
The task uses our product "Parsing Expression Grammar" to define our custom DSL. Line 18, new FlexibleConfig.Parser(), is the exported CSharp version of the PEG rules. Since our grammar allows the developer to define explicitly what gets captured, our first integration test verifies for a known input. We expect that the parser will always return 7 child nodes. This helped in forming/editing the rules for FlexibleConfig.Parser.
My second integration test is the complete processor used by the MSBuild task internally. This test uses Moq to remove the logging dependency from the integration test. We compare the output of the processed file with an expected output we have saved in the file system. This guarantees that as we modify the internals of our interpreter, the expected output should never change.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Moq;
using NUnit.Framework;
using System.IO;
using RobustHaven.Text.Npeg;
namespace FlexibleConfig.Tests
{
[TestFixture]
public class IntegrationTests
{
[Test]
public void Test_Parser_AST()
{
var parser = new FlexibleConfig.Parser();
var path = Path.Combine(Environment.CurrentDirectory, @"ParserData\App.debug.config");
var astnode = parser.Parse(new StringInputIterator(System.IO.File.ReadAllText(path)));
Assert.IsNotNull(astnode);
Assert.IsTrue(astnode.Children.Count == 7);
}
[Test]
public void Test_Preprocessor()
{
var logger = new Mock<ILogger>();
var path = Path.Combine(Environment.CurrentDirectory, @"ParserData\App.debug.config");
var parameters = new Dictionary<String, String>();
parameters.Add("MSBuildStartupDirectory", Environment.CurrentDirectory);
var processor = new FlexibleConfig.Processor(logger.Object, File.ReadAllText(path), parameters);
var output = processor.Execute();
Assert.IsTrue(File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "Expected001.config")) == output);
}
}
}
Real Life Example
We are going show you our msbuild project that we manually run after an svn update. This projects automates synchronizing configuration and database changes in the project.
<Project ToolsVersion="3.5" DefaultTargets="Setup" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <MSBuildCommunityTasksPath>$(MSBuildStartupDirectory)\Externs\msbuild\MSBuild.Community.Tasks\Build</MSBuildCommunityTasksPath> </PropertyGroup> <Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets"/> <PropertyGroup> <MSSDCTasksPath>$(MSBuildStartupDirectory)\Externs\msbuild\SDC.Tasks</MSSDCTasksPath> </PropertyGroup> <Import Project="$(MSSDCTasksPath)\Microsoft.Sdc.Common.tasks"/> <PropertyGroup> <RobustHavenTasksPath>$(MSBuildStartupDirectory)\Externs\msbuild\RobustHaven.Tasks</RobustHavenTasksPath> </PropertyGroup> <Import Project="$(RobustHavenTasksPath)\RobustHaven.Tasks.Targets"/> <Target Name="Shared"> <ItemGroup> <FlexibleConfigParameters Include="MSBuildStartupDirectory"> <Key>MSBuildStartupDirectory</Key> <Value>$(MSBuildStartupDirectory)\MSBuild</Value> </FlexibleConfigParameters> </ItemGroup> <Message Text="Creating ConnectionString.config" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(FlexibleConfigParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\ConnectionString.config" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\ConnectionString.config" /> <Message Text="Creating MailSettings.config" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(FlexibleConfigParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\MailSettings.config" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\MailSettings.config" /> <Message Text="Creating MSBuild.config" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(FlexibleConfigParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\MSBuild.config" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\MSBuild.config" /> <Message Text="Executing - Database Change Management" /> <DBChangeManagement.MSBuildTask.MSSQLTask TaskConfiguration="$(MSBuildStartupDirectory)\MSBuild\bin\MSBuild.config" EnvironmentName="Development" BackupLocation="$(MSBuildStartupDirectory)\Database\robusthavenaspnetmvc.bak" DatabaseName="robusthavenaspnetmvc" ConnectionStringKey="dbhReader" UpdateScriptsLocation="$(MSBuildStartupDirectory)\Database" /> </Target> <Target Name="Update" DependsOnTargets="Shared"> <ItemGroup> <WwwParameters Include="MSBuildStartupDirectory"> <Key>MSBuildStartupDirectory</Key> <Value>$(MSBuildStartupDirectory)\MSBuild</Value> </WwwParameters> <WwwParameters Include="IsAdminWww"> <Key>IsAdminWww</Key> <Value>false</Value> </WwwParameters> </ItemGroup> <Message Text="Creating Www.Web.config" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(WwwParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Www.Web.config" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\Www.Web.config" /> <Message Text="Creating Elmah.xslt" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(WwwParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Elmah.xslt" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\Elmah.xslt" /> <Message Text="Merging Elmah component" /> <Exec Command="..\Externs\msbuild\ExecTasks\AltovaXML.exe /in "$(MSBuildStartupDirectory)\MSBuild\bin\Www.Web.config" /xslt2 "$(MSBuildStartupDirectory)\MSBuild\bin\Elmah.xslt"" /> <Exec Command="move /Y "$(MSBuildStartupDirectory)\MSBuild\bin\xslt.output" "$(MSBuildStartupDirectory)\MSBuild\bin\tmp.output"" /> <Message Text="Merging RobustHaven.Controls component" /> <Exec Command="..\Externs\msbuild\ExecTasks\AltovaXML.exe /in "$(MSBuildStartupDirectory)\MSBuild\bin\tmp.output" /xslt2 "$(MSBuildStartupDirectory)\MSBuild\RobustHaven.Controls.xslt"" /> <Exec Command="move /Y "$(MSBuildStartupDirectory)\MSBuild\bin\xslt.output" "$(MSBuildStartupDirectory)\MSBuild\bin\tmp.output"" /> <Message Text="Merging RobustHaven.Product component" /> <Exec Command="..\Externs\msbuild\ExecTasks\AltovaXML.exe /in "$(MSBuildStartupDirectory)\MSBuild\bin\tmp.output" /xslt2 "$(MSBuildStartupDirectory)\MSBuild\RobustHaven.Products.xslt"" /> <Message Text="Moving Www.Web.config" /> <Exec Command="move /Y "$(MSBuildStartupDirectory)\MSBuild\bin\xslt.output" "$(MSBuildStartupDirectory)\RobustHaven.Www.WebApplication\Web.config"" /> <ItemGroup> <AdminParameters Include="MSBuildStartupDirectory"> <Key>MSBuildStartupDirectory</Key> <Value>$(MSBuildStartupDirectory)\MSBuild</Value> </AdminParameters> <AdminParameters Include="IsAdminWww"> <Key>IsAdminWww</Key> <Value>true</Value> </AdminParameters> </ItemGroup> <Message Text="Creating Admin.Web.config" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(AdminParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Admin.Web.config" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\Admin.Web.config" /> <Message Text="Creating Elmah.xslt" /> <FlexibleConfig.FlexibleConfigTask Parameters="@(AdminParameters)" BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Elmah.xslt" ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log" OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\Elmah.xslt" /> <Message Text="Merging Elmah component" /> <Exec Command="..\Externs\msbuild\ExecTasks\AltovaXML.exe /in "$(MSBuildStartupDirectory)\MSBuild\bin\Admin.Web.config" /xslt2 "$(MSBuildStartupDirectory)\MSBuild\bin\Elmah.xslt"" /> <Exec Command="move /Y "$(MSBuildStartupDirectory)\MSBuild\bin\xslt.output" "$(MSBuildStartupDirectory)\MSBuild\bin\tmp.output"" /> <Message Text="Merging RobustHaven.Controls component" /> <Exec Command="..\Externs\msbuild\ExecTasks\AltovaXML.exe /in "$(MSBuildStartupDirectory)\MSBuild\bin\tmp.output" /xslt2 "$(MSBuildStartupDirectory)\MSBuild\RobustHaven.Controls.xslt"" /> <Message Text="Moving Admin.Web.config" /> <Exec Command="move /Y "$(MSBuildStartupDirectory)\MSBuild\bin\xslt.output" "$(MSBuildStartupDirectory)\RobustHaven.Admin.WebApplication\Web.config"" /> </Target> </Project>
This is the results after running our msbuild project after the svn update.
The color code:
- Red Highlight - show where our task FlexibleConfig.FlexibleConfigTask is being used.
- Purple Highlight - shows where our task DBChangeManagement.MSBuildTask.MSSQLTask is being used.
- Yellow Highlight - show where AltovaXML.exe is used to inject sections into specific locations in an xml config.
The "shared" target generates custom configs that will be included by other tasks or configs during the "update" task. Here are a couple of examples of this:
- When MSBuildBaseline.config is created the Database Change Management task consumes the auto-generated config file that contains the user/environment specific connection string from line 31. See Line 31, 47, 53.
- Elmah xslt file is also auto generated depending on if we plan on creating admin.web.config or front end www.web.config. So when Elmah section is merged the appropriate handlers will be merged in. See line: 92, 96, 138, 142
Here is the Elmah.xslt baseline file which is fed into the FlexibleConfigTask to autogenerate the Elmah.xslt file fed into altovaxml to merge for admin and www subdomain configs.
<flexibleconfig path="$(MSBuildStartupDirectory)\useroptions.include" />
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" omit-xml-declaration="no" name="xmlFormat" indent="yes" />
<xsl:template match="/">
<xsl:result-document href="bin\xslt.output" format="xmlFormat">
<xsl:apply-templates />
</xsl:result-document>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="configuration/configSections">
<configSections>
<xsl:apply-templates select="@* | *"/>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
</sectionGroup>
</configSections>
<?flexibleconfig
switch(DevId)
{
case "production":
?>
<?=System.IO.File.ReadAllText(UserConfigurationPath + @"\elmah.config") ?>
<?flexibleconfig
break;
default:
?>
<elmah>
<security allowRemoteAccess="yes" />
<errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="dbhReader" applicationName="Www.WebApp" />
<errorMail from="noreply@example.com" to="elmah@example.com" priority="high" />
<errorFilter>
<test>
<equal binding="HttpStatusCode" value="404" type="Int32" />
</test>
</errorFilter>
<!--<errorMail
from="elmah@example.com"
to="admin@example.com"
subject="..."
priority="Low|Normal|High"
async="true|false"
smtpPort="25"
smtpServer="smtp.example.com"
useSsl="true|false"
userName="johndoe"
password="secret"
noYsod="true|false" />-->
</elmah>
<?flexibleconfig
break;
}
?>
</xsl:template>
<xsl:template match="configuration/system.web/httpModules">
<httpModules>
<xsl:apply-templates select="@* | *"/>
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah"/>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
</httpModules>
</xsl:template>
<xsl:template match="configuration/system.webServer/modules">
<modules runAllManagedModulesForAllRequests="{@runAllManagedModulesForAllRequests}">
<xsl:apply-templates select="@* | *"/>
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah"/>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
</modules>
</xsl:template>
<?flexibleconfig
if( "$(IsAdminWww)" == "true")
{
?>
<xsl:template match="configuration">
<configuration>
<xsl:apply-templates select="@* | *"/>
<location path="elmah.axd">
<system.web>
<authorization>
<allow roles="ErrorLog.Administrator"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
</configuration>
</xsl:template>
<xsl:template match="configuration/appSettings">
<appSettings>
<xsl:apply-templates select="@* | *"/>
<add key="Elmah.ErrorView" value="AdminError"/>
</appSettings>
</xsl:template>
<xsl:template match="configuration/system.web/httpHandlers">
<httpHandlers>
<xsl:apply-templates select="@* | *"/>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</httpHandlers>
</xsl:template>
<xsl:template match="configuration/system.webServer/handlers">
<handlers>
<xsl:apply-templates select="@* | *"/>
<add name="Elmah" verb="POST,GET,HEAD" path="elmah.axd" preCondition="integratedMode" type="Elmah.ErrorLogPageFactory, Elmah"/>
</handlers>
</xsl:template>
<?flexibleconfig
}
?>
</xsl:stylesheet>
All comments on this article are moderated
