Web.Config Transformations in VS2010 vs FlexibleConfig.FlexibleConfigTask

Problem: Web.Config Transformations coupled to Solution Configurations

If you do not know what Web.Config Transformations are, please take a look at Chris Koenig’s “Hot Do I” video series on the subject.

One of the problems that I see with these types of transformations is that they are highly dependent on “Solution Configurations”. Since we have 3 environments/DevEnvironments to support, i.e. integration, regression, and uat, 3 solution configurations had to be created with 3 transformation files. Even using small teams, a DevId had to be supported in a connectionString section, in-case the team is distributed (not collocated).

Whilst working in Silverlight or WPF, it is quite common to have a developer work on UI using mocked services; whilst another developer ties the data services layer with the concrete service versions. A DevTask also needs to be considered in the build, to support this configuration.

Should the Solution Configurations approach be taken to support DevId, DevEnvironment, DevTask, and/or DevBranch, it would quickly become unmanageable with Web.config Transformations, as too many extra files would be needed.

Problem: Web.Config Transformations does not encourage separating module configurations

Using PRISM for both Silverlight and WPF I’ve become accustomed to breaking down projects into modules. This idea of modules is now seen in ASP.NET MVC 2 with introduction to Areas.

It is not uncommon for each module/area to have a custom configuration. Web.Config Transformations does not offer any help in merging these separated configurations.

Solution: Use FlexibleConfig.FlexibleConfigTask with AltovaXML

FlexibleConfig.FlexibleConfigTask provides the C# conditional evaluation of configuration files.

The following sample evaluates the connectionStrings by considering the DevId, DevBranch, DevEnvironment, and ProductName.

          <?flexibleconfig
string dbInstance = string.Empty;

string dbCompany = string.Empty;

string prefix = string.Empty;

dbInstance = ProductName + DevEnvironment;

if( DevEnvironment == "PRODUCTION")
{
  if(DevEnvironment == "PRODUCTION")
  dbInstance = "ProductNameAPP";

dbCompany = "Company";
}
else if( DevEnvironment == "DEV" || DevEnvironment == "TEST" || DevEnvironment == "UAT" )
{
if(DevEnvironment == "UAT")
	dbInstance = "ProductNameDB";

if(DevBranch == "DEMO" || DevBranch == "TRAINING" || DevBranch == "MODELOFFICE")
	dbInstance = "ProductNameUAT";

  prefix = ProductName + DevEnvironment + DevBranch;
  dbCompany = prefix + "Company";
}
else
{
  prefix = DevBranch != "TRUNK-WEBAPP" ? ProductName + DevEnvironment + DevBranch:"";
  dbCompany = prefix + "Company";

switch(DevId)
{
	case "LEBLANCMENESES":
		if(DevEnvironment == "HOME.LAPTOP")
		{
			dbInstance = @"JANIELAPTOP\MSSQLSERVER1";
		}
		else if(DevEnvironment == "HOME.DESKTOP")
		{
			dbInstance = @".";
		}
		else if(DevEnvironment == "HOME.VM")
		{
			dbInstance = @".";
		}
	break;

	case "EFLORES":
	case "DENIS":
        dbInstance = @".";
      break;
	default:
		throw new ArgumentException("DevId not defined when establishing a connection string.");
}
}
?>

        

AltovaXML.exe is a free download that provides the XSLT transformation required to merge the separated xml configuration files.

It is a command line tool that is often called as follows: AltovaXML.exe /in inputfile.xml /xslt2 transform.xslt

When consumed in an MSBuild project, I use Exec task to call external command line tools.

An XSLT template that does nothing to the input file is known as the identity template. This goes though all the nodes in the document and does not have a template to match, so the input file is pushed to the output unchanged.

          <?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

        

A more practical example is merging Enterprise Library blocks into a web.config or app.config Here is an example of our entlib.xslt file using most recent Enterprise Library 5.0:

          <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="xml" indent="yes" xml:space="preserve" />

<xsl:template match="/">
	<xsl:apply-templates />
</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="@*|node()"/>
		<section name="loggingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
		<section name="exceptionHandling" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Configuration.ExceptionHandlingSettings, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
	</configSections>

	<loggingConfiguration name="Logging Application Block" tracingEnabled="true" defaultCategory="General">
		<listeners>
			<add name="Event Log Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FormattedEventLogTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
				listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FormattedEventLogTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
				source="<?="$(ExceptionPolicy)"  ?>" formatter="Text Formatter" log="Application"
				machineName="" traceOutputOptions="None" />
			<add name="Rolling Flat File Trace Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.RollingFlatFileTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
				listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.RollingFlatFileTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
				fileName="rolling2.log" formatter="Text Formatter" rollSizeKB="500000"
				traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, ProcessId, ThreadId, Callstack" />

          <xsl:if test="'<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCapture' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCaptureBridge' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.ServiceMonitor'" >
            <add listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.CustomTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" type="EntLib.Server.ElmahTraceListener, EntLib.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Elmah Trace Listener" formatter="Text Formatter"/>
          </xsl:if>
		</listeners>
		<formatters>
			<add type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
				template="Timestamp: {{timestamp}}{{newline}}&#xA;Message: {{message}}{{newline}}&#xA;Category: {{category}}{{newline}}&#xA;Priority: {{priority}}{{newline}}&#xA;EventId: {{eventid}}{{newline}}&#xA;Severity: {{severity}}{{newline}}&#xA;Title:{{title}}{{newline}}&#xA;Machine: {{localMachine}}{{newline}}&#xA;App Domain: {{localAppDomain}}{{newline}}&#xA;ProcessId: {{localProcessId}}{{newline}}&#xA;Process Name: {{localProcessName}}{{newline}}&#xA;Thread Name: {{threadName}}{{newline}}&#xA;Win32 ThreadId:{{win32ThreadId}}{{newline}}&#xA;Extended Properties: {{dictionary({{key}} - {{value}}{{newline}})}}"
				name="Text Formatter" />
		</formatters>
		<categorySources>
			<add switchValue="All" name="General">
				<listeners>
				</listeners>
			</add>
			<add switchValue="All" name="Information">
				<listeners>
					<add name="Event Log Listener"/>
            <xsl:if test="'<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCapture' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCaptureBridge' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.ServiceMonitor'">
					  <add name="Elmah Trace Listener"/>
            </xsl:if>
				</listeners>
			</add>
			<add switchValue="All" name="Warning">
				<listeners>
					<add name="Event Log Listener"/>
            <xsl:if test="'<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCapture' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCaptureBridge' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.ServiceMonitor'">
              <add name="Elmah Trace Listener"/>
            </xsl:if>
				</listeners>
			</add>
			<add switchValue="All" name="Errors">
				<listeners>
					<add name="Event Log Listener"/>
            <xsl:if test="'<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCapture' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCaptureBridge' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.ServiceMonitor'">
              <add name="Elmah Trace Listener"/>
            </xsl:if>
				</listeners>
			</add>
        <xsl:if test="'<?="$(ExceptionPolicy)"?>'='AVSTX.POS.ServiceMonitor'">
          <add switchValue="All" name="Email">
            <listeners>
            </listeners>
          </add>
        </xsl:if>
		</categorySources>
		<specialSources>
			<allEvents switchValue="All" name="All Events" />
			<notProcessed switchValue="All" name="Unprocessed Category" />
			<errors switchValue="All" name="Logging Errors &amp; Warnings">
				<listeners>
					<add name="Event Log Listener" />
            <xsl:if test="'<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCapture' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.DataCaptureBridge' and '<?="$(ExceptionPolicy)"?>'!='AVSTX.POS.ServiceMonitor'">
              <add name="Elmah Trace Listener"/>
            </xsl:if>
				</listeners>
			</errors>
		</specialSources>
	</loggingConfiguration>
	<exceptionHandling>
		<exceptionPolicies>
			<add name="<?="$(ExceptionPolicy)"  ?>">
				<exceptionTypes>
					<add name="All Exceptions" type="System.Exception, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
						postHandlingAction="None">
						<exceptionHandlers>
							<add name="Logging Exception Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Logging.LoggingExceptionHandler, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
								logCategory="Errors" eventId="100" severity="Error" title="Enterprise Library Exception Handling"
								formatterType="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.TextExceptionFormatter, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling"
								priority="0" />
						</exceptionHandlers>
					</add>
				</exceptionTypes>
			</add>
		</exceptionPolicies>
	</exceptionHandling>
</xsl:template>
</xsl:stylesheet>

        

Put It Together

The following image shows one of our projects with its associated modules. Each module normally contains mock services, an MSBuild project file, and anything else specific to the module. modules seperated from main project. The rest of the example shows the project highlighted in red.

The trick to getting this to work is to predefine: $(DevId) $(DevEnvironment) $(DevTask) inside environment variables that enable access to both C# conditions inside FlexibleConfigTask and inside all the MSBuild project files.

The trick to support configurable module development is in the following target and specifically on line 35.

          <Target Name="ModuleSync">
	<ItemGroup>
		<Modules Include="SecurityModule">
			<ModuleName>SecurityModule</ModuleName>
			<ServiceReferencesPath></ServiceReferencesPath>
			<WebConfigPath>$(Host)\Web.$(ConfigurationNameSuffix)</WebConfigPath>
			<SvcPath>$(Host)\Services</SvcPath>
		</Modules>
		<Modules Include="PolicyModule">
			<ModuleName>PolicyModule</ModuleName>
			<ServiceReferencesPath></ServiceReferencesPath>
			<WebConfigPath>$(Host)\Web.$(ConfigurationNameSuffix)</WebConfigPath>
			<SvcPath>$(Host)\Services</SvcPath>
		</Modules>
	</ItemGroup>

	<MSBuild Projects="$(ExternsMSBuildFolder)\Module.proj"
		Targets="ModuleSync"
		Properties="ModuleName=%(Modules.ModuleName);
		TemporaryFolder=$(TemporaryFolder);
		MyMSBuildFolder=$(MyMSBuildFolder);
		SvcPath=%(SvcPath);
		ServiceReferencesPath=%(ServiceReferencesPath);
		WebConfigPath=%(WebConfigPath);
		FlexibleConfigTaskDevIdName=CropDevId;
		FlexibleConfigTaskDevId=$(CropDevId);
		FlexibleConfigTaskDevEnvironmentName=CropDevEnvironment;
		FlexibleConfigTaskDevEnvironment=$(CropDevEnvironment);
		FlexibleConfigTaskDevTaskName=CropDevTask;
		FlexibleConfigTaskDevTask=$(CropDevTask)" />
</Target>

        

Module.proj referenced in the script is:

          <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
	<RobustHavenTasksPath>$(MSBuildProjectDirectory)\RobustHaven.Tasks</RobustHavenTasksPath>
</PropertyGroup>
<Import Project="$(RobustHavenTasksPath)\RobustHaven.Tasks.Targets"/>



<ItemGroup>
    <FlexibleConfigParameters Include="MSBuildStartupDirectory">
      <Key>MSBuildStartupDirectory</Key>
      <Value>$(MyMSBuildFolder)</Value>
    </FlexibleConfigParameters>
    <FlexibleConfigParameters Include="$(FlexibleConfigTaskDevIdName)">
      <Key>$(FlexibleConfigTaskDevIdName)</Key>
      <Value>$(FlexibleConfigTaskDevId)</Value>
    </FlexibleConfigParameters>
    <FlexibleConfigParameters Include="$(FlexibleConfigTaskDevLocationName)">
      <Key>$(FlexibleConfigTaskDevLocationName)</Key>
      <Value>$(FlexibleConfigTaskDevLocation)</Value>
    </FlexibleConfigParameters>
    <FlexibleConfigParameters Include="$(FlexibleConfigTaskDevTaskName)">
      <Key>$(FlexibleConfigTaskDevTaskName)</Key>
      <Value>$(FlexibleConfigTaskDevTask)</Value>
    </FlexibleConfigParameters>
</ItemGroup>


<Target Name="ModuleSync">
	<CallTarget Targets="ServiceDeployment" />
	<CallTarget Condition="Exists('$(MyMSBuildFolder)\Modules\$(ModuleName)\ServiceReferences.xslt')" Targets="ServiceReferencesConfig" />
	<CallTarget Condition="Exists('$(MyMSBuildFolder)\Modules\$(ModuleName)\Web.xslt')" Targets="WebConfig" />
</Target>


<Target Name="ServiceDeployment">
	<ItemGroup>
		<SvcFiles Include="$(MyMSBuildFolder)\Modules\$(ModuleName)\*.svc" />
		<SvcFiles Condition="$(FlexibleConfigTaskDevTask) == 'UI'" Include="$(MyMSBuildFolder)\Modules\$(ModuleName)\MockServices\*.svc" />
	</ItemGroup>
	<Message Text="@(SvcFiles)" />
	<Copy
            SourceFiles="@(SvcFiles)"
            DestinationFolder="$(SvcPath)\$(ModuleName)"
        />
</Target>



<Target Name="ServiceReferencesConfig">
	<Message Text="$(ModuleName): Merging ServiceReferences.xslt" />
	<FlexibleConfig.FlexibleConfigTask
		Parameters="@(FlexibleConfigParameters)"
		BaseLineFile="$(MyMSBuildFolder)\Modules\$(ModuleName)\ServiceReferences.xslt"
		ErrorLogFile="$(TemporaryFolder)\Error.log"
		OutputFile="$(TemporaryFolder)\ServiceReferences.xslt"
	/>
	<XslTransformation
		OutputPaths="$(TemporaryFolder)\xslt.output"
		XmlInputPaths="$(ServiceReferencesPath)\ServiceReferences.ClientConfig"
		XslInputPath="$(TemporaryFolder)\ServiceReferences.xslt"
	/>
	<Copy
            SourceFiles="$(TemporaryFolder)\xslt.output"
            DestinationFiles="$(ServiceReferencesPath)\ServiceReferences.ClientConfig"
        />
</Target>



<Target Name="WebConfig">
	<Message Text="$(ModuleName): Merging Web.xslt" />
	<FlexibleConfig.FlexibleConfigTask
		Parameters="@(FlexibleConfigParameters)"
		BaseLineFile="$(MyMSBuildFolder)\Modules\$(ModuleName)\Web.xslt"
		ErrorLogFile="$(TemporaryFolder)\Error.log"
		OutputFile="$(TemporaryFolder)\Web.xslt"
	/>
	<XslTransformation
		OutputPaths="$(TemporaryFolder)\xslt.output"
		XmlInputPaths="$(WebConfigPath)"
		XslInputPath="$(TemporaryFolder)\Web.xslt"
	/>
	<Copy
            SourceFiles="$(TemporaryFolder)\xslt.output"
            DestinationFiles="$(WebConfigPath)"
        />
</Target>
</Project>