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/DevLocations to support, i.e. debug, release, and staging, 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, DevLocation, and/or DevTask, 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 and DevLocation.

 
  <connectionStrings>
   <clear />
	
  <?flexibleconfig
switch(DevId)
{
	case "leblanc":
		if(DevLocation == "home.desktop")
		{
			?><add name="dbh" connectionString="Persist Security Info=False;Integrated Security=true;Initial Catalog=RobustHaven;server=Leblanc-PC\MSSQLSERVER1"/>
			<?flexibleconfig
		}
		else if(DevLocation == "work")
		{
			?><add name="dbh" connectionString="Persist Security Info=False;Integrated Security=true;Initial Catalog=RobustHaven;server=(local);"/>
			<?flexibleconfig
		}
		
		break;
	
	default:
		throw new ArgumentException("DevId not defined when establishing a connection string.");
}


if (DevLocation == "qa")
{
	?><add name="dbh" connectionString="Persist Security Info=False; Integrated Security=true; Initial Catalog=RobustHaven;server=qabox"/>
	<?flexibleconfig
}
else if (DevLocation == "integration")
{
	?><add name="dbh" connectionString="Persist Security Info=False; Integrated Security=true; Initial Catalog=RobustHaven;server=integration"/>
	<?flexibleconfig
}
?>
  </connectionStrings>

	

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" 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>
				<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="AVSTX.POS" 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" />
					<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" />
				</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>
							<add name="Rolling Flat File Trace Listener" />
						</listeners>
					</add>
					<add switchValue="All" name="Information">
						<listeners>
							<add name="Event Log Listener" />
							<add name="Elmah Trace Listener" />
						</listeners>
					</add>
					<add switchValue="All" name="Warning">
						<listeners>
							<add name="Event Log Listener" />
							<add name="Elmah Trace Listener" />
						</listeners>
					</add>
					<add switchValue="All" name="Errors">
						<listeners>
							<add name="Event Log Listener" />
							<add name="Elmah Trace Listener" />
						</listeners>
					</add>
				</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" />
							<add name="Elmah Trace Listener" />
						</listeners>
					</errors>
				</specialSources>
			</loggingConfiguration>
			<exceptionHandling>
				<exceptionPolicies>
					<add name="AVSTX.POS">
						<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: $(BusinessIntelligenceViewDevId) $(BusinessIntelligenceViewLocation) $(BusinessIntelligenceViewTask) inside environment variables that enable access to both C# conditions inside FlexibleConfigTask and inside all the MSBuild project files.

The base MSBuild format for all my projects is (Line 88 is the target.):

 
		<Project ToolsVersion="3.5" DefaultTargets="Update" 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"/>

			
			
			<ItemGroup>
				<FlexibleConfigParameters Include="MSBuildStartupDirectory">
				  <Key>MSBuildStartupDirectory</Key>
				  <Value>$(MSBuildStartupDirectory)\MSBuild</Value>
				</FlexibleConfigParameters>
			</ItemGroup>

			
			<Import Project="$(MSBuildStartupDirectory)\MSBuild\Components\News\News.proj" />
			<Import Project="$(MSBuildStartupDirectory)\MSBuild\Components\CustomerManagement\CustomerManagement.proj" />
			<Import Project="$(MSBuildStartupDirectory)\MSBuild\Components\HumanResources\HumanResources.proj" />
			<Import Project="$(MSBuildStartupDirectory)\MSBuild\Components\Network\Network.proj" />
			<Import Project="$(MSBuildStartupDirectory)\MSBuild\Components\Security\Security.proj" />
			
			
			
			<Target Name="DatabaseChangesSync">	
				<Message Text="Merging MSBuild.config" />
				<FlexibleConfig.FlexibleConfigTask
					Parameters="@(FlexibleConfigParameters)"
					ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\error.log"
					BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\MSBuildBaseline.config"
					OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\MSBuild.config"
				/>
				
				<Message Text="Executing - Database Change Management" />
				<DBChangeManagement.MSBuildTask.MSSQLTask
					TaskConfiguration="$(MSBuildStartupDirectory)\MSBuild\bin\MSBuild.config"
					EnvironmentName="Development"
					ConnectionStringKey="MSSQLTask"
					DatabaseName="BusinessIntelligenceView"
					BackupLocation="$(MSBuildStartupDirectory)\Database\BusinessIntelligenceView.bak"
					UpdateScriptsLocation="$(MSBuildStartupDirectory)\Database"
				/>
			</Target>
			
			
			

			<Target Name="SyncConfiguration">
				<Message Text="Executing ServiceReferences.ClientConfig" />
				<FlexibleConfig.FlexibleConfigTask
					Parameters="@(FlexibleConfigParameters)"
					ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\error.log"
					BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\ServiceReferences.ClientConfig"
					OutputFile="$(MSBuildStartupDirectory)\BusinessIntelligence\ServiceReferences.ClientConfig"
				/>
				
				<Message Text="Executing - Web.config" />
				<FlexibleConfig.FlexibleConfigTask
					Parameters="@(FlexibleConfigParameters)"
					ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\error.log"
					BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Web.config"
					OutputFile="$(MSBuildStartupDirectory)\BusinessIntelligence.Web\Web.config"
				/>
				
				<Message Text="Executing - Services.Web.config" />
				<FlexibleConfig.FlexibleConfigTask
					Parameters="@(FlexibleConfigParameters)"
					ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\error.log"
					BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Services.Web.config"
					OutputFile="$(MSBuildStartupDirectory)\BusinessIntelligence.Web\Services\Web.config"
				/>
			</Target>

			
			
			
			<Target Name="Update">
				<Message Text="Assigning Local Variables" />
			
				<Microsoft.Sdc.Tasks.GetEnvironmentVariable Variable="BusinessIntelligenceViewDevId" Target="User">
					<Output PropertyName="BusinessIntelligenceViewDevId" TaskParameter="Value" />
				</Microsoft.Sdc.Tasks.GetEnvironmentVariable>
				<Microsoft.Sdc.Tasks.GetEnvironmentVariable Variable="BusinessIntelligenceViewLocation" Target="User">
					<Output PropertyName="BusinessIntelligenceViewLocation" TaskParameter="Value" />
				</Microsoft.Sdc.Tasks.GetEnvironmentVariable>
				<Microsoft.Sdc.Tasks.GetEnvironmentVariable Variable="BusinessIntelligenceViewTask" Target="User">
					<Output PropertyName="BusinessIntelligenceViewTask" TaskParameter="Value" />
				</Microsoft.Sdc.Tasks.GetEnvironmentVariable>
				
				<Message Text=" $(BusinessIntelligenceViewDevId) $(BusinessIntelligenceViewLocation) $(BusinessIntelligenceViewTask)" />
				
				<CallTarget Targets="SyncConfiguration" />
				<CallTarget Targets="DatabaseChangesSync" />
				<CallTarget Targets="ModulesSync" />
				<CallTarget Targets="LoggingConfiguration" />
						
			</Target>
			
			
			
			<Target Name="ModulesSync">
				<CallTarget Targets="NewsSync" />
				<CallTarget Targets="CustomerManagementSync" />
				<CallTarget Targets="HumanResourcesSync" />
				<CallTarget Targets="NetworkSync" />
				<CallTarget Targets="SecuritySync" />
			</Target>
			
			
			<Target Name="LoggingConfiguration">
				<Message Text="$(ModuleName): Merging loggingConfiguration.xslt" />
				<FlexibleConfig.FlexibleConfigTask
					Parameters="@(FlexibleConfigParameters)"
					BaseLineFile="$(MSBuildStartupDirectory)\MSBuild\Components\loggingConfiguration.xslt"
					ErrorLogFile="$(MSBuildStartupDirectory)\MSBuild\bin\Error.log"
					OutputFile="$(MSBuildStartupDirectory)\MSBuild\bin\loggingConfiguration.xslt"
				/>
				<Exec Command="..\Externs\msbuild\ExecTasks\AltovaXML.exe  /in &quot;$(MSBuildStartupDirectory)\BusinessIntelligence.Web\Web.config&quot; /xslt2 &quot;$(MSBuildStartupDirectory)\MSBuild\bin\loggingConfiguration.xslt&quot;" />
				<Exec Command="move /Y &quot;$(MSBuildStartupDirectory)\MSBuild\bin\xslt.output&quot; &quot;$(MSBuildStartupDirectory)\BusinessIntelligence.Web\Web.config&quot;" />
			</Target>

		</Project>
	

Line 105 tells each module build file to begin the merge process by first evaluating conditions using FlexibleConfigTask. Subsequently, you should use AltovaXML.xml to merge the evaluated configs to ServiceReferences.ClientConfig, Web.config or any other custom config that might be a resource in a module.

 
Author: Leblanc Meneses
 
Gravatar

Leblanc has years of industry experience in all aspects of the software development life cycle. He has gathered and managed requirements, and has demonstrated his object-oriented architecture design with many UML and ER diagrams. He has also established his implementation abilities using a test-driven development approach, utilizing various languages and platforms. He has implemented continuous integration tools to maintain and integrate developed products.

He is currently a Silverlight/WPF/ASP.NET MVC/WCF Consultant in Dallas, Texas, and always welcomes inquiries from potential clients in Fort Worth, Dallas, Irving, Richardson, and Plano.

twitter @leblancmeneses linkedin

Be the first to comment!

X

Leave a Response

All comments on this article are moderated