Over the last few months I’ve seen several questions regarding setting WebDeploy parameterization values when deploying via MSBuild using the MSDeployPublish and related targets. These targets are used when publishing via Visual Studio or can be triggered by setting the MSBuild properties DeployOnBuild equal to “true” and DeployOnTarget to “MSDeployPublish”.
The typical scenario is the developer wants to deploy the web application with a environment-specific set of parameterization values. They also usually have multiple environments beyond just their local configuration.
Each time I see this question I go back and re-review the web publishing targets file which is located at (adjust for your version of VS):
C:Program Files (x86)MSBuildMicrosoftVisualStudiov14.0WebMicrosoft.Web.Publishing.targets
And each time I come to the same conclusion, the MSDeployPublish target does not support overriding the parameters. Although there are hints of this functionality as Steven pointed out in the most recent StackOverflow post:
The targets file contains some unused code that hints at better support for MSDeploy parameters (in
Microsoft.Web.Publishing.targets)
. It seems to me that they were in the process of addingsetParam
andsetParamFile
support in MSBuild, but then abandoned it.
In the past I have suggested to just use MSDeploy.exe to deploy. I still think this is a better approach which provide more capabilities but I’ve seen this question too many times now. So I decided to dig deeper and find a solution.
Overriding parameter values VIa a MSBUILD Item Group
Sayed Hashimi has a great post which describes how you can extend MSBuild via publish profiles to override parameterization values, complete with code samples.
http://sedodream.com/2013/03/02/MSDeployHowToUpdateAppSettingsOnPublishBasedOnThePublishProfile.aspx
In this solution you would set your parameter values appropriately in each of your publisher profiles.
This method can also be used without publishing profiles by adding the MSBuild script to the [ProjectName].wpp.targets file. This will help anyone publishing from Visual Studio using right click publish. If however, you run MSBuild from the commandline, via TFS or similiar this won’t work because you can’t pass a MSBuild ItemGroup through the MSBuild.exe commandline it only supports MSBuild Properties.
The bottom line is the MSDeployPublish target does not support overriding paramters values from the commandline. Maybe one day it will but not today.
The Solution
Since MSBuild.exe doesn’t support passing ItemGroups via the commandline, we have to build some code that will take a MSBuild Property and create the ItemGroup. I think there are two scenarios which should be supported:
- Setting parameters based on a SetParameters file
- Setting parameters selectively via a list of key/value pairs
This provides the most flexibility to users who use both methods and requires no changes to existing SetParameter files which can be used with MSDeploy in the future.
Since we need to extend MSBuild in a Web Application we will use the [ProjectName].wpp.target file which is a natively supported extension point. Simply create a file in the root of your web project with this name.
To cover the the first use case, first we create a target that runs before the MSDeployPublish target but after the OnAfterPipelineCollectFilesPhase target:
<?xml version="1.0" encoding="utf-8" ?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <OnAfterPipelineCollectFilesPhase> $(OnAfterPipelineCollectFilesPhase); MSDeploySetParametersFile; </OnAfterPipelineCollectFilesPhase> </PropertyGroup> <Target Name="MSDeploySetParametersFile" Condition="'$(MSDeployPublishSetParametersFile)'!=''" BeforeTargets="MSDeployPublish"> </Target>
You will also notice we added a condition to only run the Target if the MSDeployPublishSetParametersFile MSBuild property is set to a value. This is the property that we will use to pass in the path to our SetParameters.*.xml file. Here are the contents of my sample SetParameters files:
<?xml version="1.0" encoding="utf-8"?> <parameters> <setParameter name="IIS Web Application Name" value="Default Web Site/app12" /> <setParameter name="testSetting" value="changed_fromSetParamFile" /> </parameters>
Next we have to parse the SetParameters file. For this we use the XmlPeek MSBuild Task which allows us to pull XML nodes based on an XPath expression:
<XmlPeek XmlInputPath="$(MSDeployPublishSetParametersFile)" Query="/child::node()/child::node()"> <Output TaskParameter="Result" ItemName="SetParameterNodes" /> </XmlPeek>
Here you see we pass the SetParameters file path and our XPath query. The XPath is super generic because MSDeploy doesn’t actually care what node names you use it just looks for the keys and values. So we use the child::node() directive to generically get the child set param nodes regardless of their name. The results are output to an ItemGroup named SetParameterNodes, which in this case are the setParameter XML nodes as strings.
From the XML nodes we need to break out the keys and values into a new ItemGroup. For this we use Regex to query for the parameter name which is set to the Identity of the ItemGroup and the value is set to the the value ItemGroup metadata property.
<ItemGroup> <SetParameters Include="$([System.Text.RegularExpressions.Regex]::Match(%(SetParameterNodes.Identity), `(?&lt;=name=&quot;)[^&quot;]*`))"> <value>$([System.Text.RegularExpressions.Regex]::Match(%(SetParameterNodes.Identity), `(?&lt;=value=&quot;)[^&quot;]*`))</value> </SetParameters> </ItemGroup>
Finally we use Sayed’s post to set the MsDeployDeclareParameters ItemGroup for each key/value pair using MSBuild item batching.
<ItemGroup> <MsDeployDeclareParameters Include="%(SetParameters.Identity)"> <DefaultValue>%(SetParameters.value)</DefaultValue> <Priority>$(MSDeployPublishSetParametersPriority)</Priority> </MsDeployDeclareParameters> </ItemGroup>
We also set the priority as Sayed described in his post.
The second use case is easier as we just need to take a string MSBuild Property set on the commandline, convert it to an ItemGroup and parse out the keys/values using Regex.
<ItemGroup> <SetParameterArguments Include="$(MSDeployPublishSetParameters)" /> <SetParameters Include="$([System.Text.RegularExpressions.Regex]::Match(%(SetParameterArguments.Identity), `[^=]*(?==)`))"> <value>$([System.Text.RegularExpressions.Regex]::Match(%(SetParameterArguments.Identity), `(?&lt;==')[^']*`))</value> </SetParameters> </ItemGroup>
From there you can use the same snippet above to set the MsDeployDeclareParameters ItemGroup.
I shared the final script on GitHub as a Gist. Check it out –
https://gist.github.com/rschiefer/428a8c6de9dfc43e8b48c8181970fc9f
Note – I did refactor this a bit to remove some duplication and added some comments to help the user.
Usage
By extending MSBuild with this script users can override parameter values with ease from the MSBuild.exe commandline call:
msdeploy.exe ... /p:MSDeployPublishSetParametersFile=SetParameters.Test.xml msdeploy.exe ... /p:MSDeployPublishSetParameters=testSetting='changed_fromSetParam';IIS Web Application Name='Default Web Site/app13'
One more thing
Before letting you go, I have to reiterate how much better MSDeploy is for deploying applications. MSBuild was not built for deployments, it was built to compile/build code. I challenge you to get out of your comfort zone a little and at least try it. I promise you won’t be disappointed.
What do you think? Are there scenarios, situations or issues that I’m not considering? Let me know in the comments or on Twitter.
There might be a better way to do this.
The VSMSDeploy task in Microsoft.Web.Publishing.Tasks.dll does actually support setParam and setParamFile via task parameters.
public ITaskItem[] SimpleSetParameterItems
public ITaskItem[] SetParameterItems
public ITaskItem[] ImportSetParametersItems
The MSDeployPublish target doesn’t ever set those parameters, but you can copy the entire target to the .wpp.targets file and then change it to pass those values.
Yes, that would work as well but I would consider that a more complicated solution. That’s a lot more code to copy over to wpp.targets. Plus you would have to update it each time VS is updated to make sure the functionality is up to date with the latest. Not a bad suggestion, I just think this is a better solution. Thanks for the comment!
I tried going down this path but it doesn’t even work in VS 2015. The VSMSDeploy task throws an exception when you set ‘SimpleSetParameterItems’ to a non-empty collection.
Good to know. At least we have a solution that works.