Variable substitution during deployment of .NET Framework web application

Background

I love .NET Core. One of the nice features of .NET is the ability to configure the application using a “appsettings.json” file that contains default settings, and an “appsettings.<environment>.json” that contains settings for a specific environment. This enables us developers to configure e.g. a connection string per environment. It is great to have this ability, but I’d rather not hard-code settings into “appsettings.<environment>.json”; if, in the example of the connection string, the server name changes, you have to update this in the relevant appsettings files. The solution is to use variables substitution during deployment.

When deploying a .NET Core web application to a virtual machine using an Azure Devops YAML pipeline, the task IISWebAppDeploymentOnMachineGroup is frequently used. It will let you specify one or more appsettings files on which substitution will take place, i.e. the value of variables in a given appsettings file will be replaced if a pipeline variable with the same key exists, and the new value will be the value of that pipeline variable.

The problem

Though .NET Core is great, applications written in .NET Framework are still actively being developed, and these applications also need to be deployed. The variable substitution described above, does not work, or at least I haven’t figured out how to do variables substitution with IISWebAppDeploymentOnMachineGroup. As a consultant, having worked for multiple companies, and I’ve seen the following two strategies around this issue:

The first solution is to use Publish profiles (*.pubxml). For me, this approach is suitable if you are using Visual Studio to interactively deploy to an environment. For each environment that you need to deploy to, there must be a separate pubxml. This is similar to hard-coding values in a “appsettings.<environment>.json” file.

The second approach that I’ve encountered, is to run a Powershell script during the deployment of the web application, whereby the Powershell script will modify the ‘web.config‘ file. This enables variable substitution, but if you add / rename a setting in the ‘web.config‘ file, you must also modify the Powershell script and also the pipeline that invokes that script. This is particularly challenging if you are using a traditional Release pipeline, which is not part of source control. Say you modify ‘web.config‘, the Powershell script, and the Release pipeline during the development phase, but all of a sudden you need to deploy a hotfix to Production; the production version of the Powershell script (which knows nothing about the new parameter) will be triggered by the Release, but with the new parameter, which causes the Release to fail.

The solution

Parameterization of parameters in ‘web.config‘ files of .NET Framework applications can be achieved in a two-step process.

Parameters.xml

The first step is to add a file ‘Parameters.xml‘ next to the ‘web.config’ file in the source repository. In this xml file, you specify which file and which parameters you want to parameterize. As an example, consider the the following ‘web.config’

<?xml version="1.0" encoding="utf-8"?>
<!-- File: web.config -->
<configuration>
  <appSettings>
      <add key="MySetting" value="https://my-default.value" />
  </appSettings> 
  <system.web>
     <compilation debug="true" targetFramework="4.8" />
     <httpRuntime targetFramework="4.8" />
   </system.web>
</configuration>

The first step to achieve parameterization of the appsetting “MySetting“ and the value for “debug“ is to to add the following ‘Parameters.xml‘ file:

<?xml version="1.0" encoding="utf-8" ?>
<!-- File: Parameters.xml -->
<parameters >
    <parameter name="MySetting" defaultValue="#{MySettingValue}#" >
        <parameterEntry kind="XmlFile" scope="\\web.config$" match="/configuration/appSettings/add[@key='MySetting']/@value" />
    </parameter> 
    <parameter name="CompilationDebug" defaultValue="${CompilationDebugValue}" >
        <parameterEntry kind="XmlFile" scope="\\web.config$" match="/configuration/system.web/compilation/@debug" />
    </parameter> 
</parameters>

This file contains three properties worth mentioning:

  1. Name: A unique name to identity the parameter. Note that there will be no build error if the same name is being used multiple times; the first entry is the one that counts.

  2. Default value: This property contains a tokenized value that later on will be substituted.

  3. ParameterEntry: How to locate the property subject to parameterization. In this example, XPath is used.

When building the solution, the parameter DeployOnBuild must be set to true, e.g.

msbuild <path to sln file> /p:DeployOnBuild=true ...

or if using the VsBuild@1 task

 - task: VSBuild@1
   displayName: 'Build solution solution'
   inputs:
     solution: <path to sln file>
     msbuildArgs: '/p:DeployOnBuild=true ...'

The output is a zip file containing the contents of the web application, as well as a file ‘SetParameters.xml‘ with the following contents:

<?xml version="1.0" encoding="utf-8"?>
<!-- File: SetParameters.xml, generated by MsBuild -->
<parameters>
    <setParameter name="IIS Web Application Name" value="Default Web Site/Name_of_application"/>
    <setParameter name="Mysetting" value="#{MySettingValue}#"/>
    <setParameter name="CompilationDebug" value="#{CompilationDebugValue}#"/>
</parameters>

The zip file not only contains assemblies and the ‘web.config‘, but also the ‘Parameters.xml’ file described earlier. When deploying using msdeploy.exe (e.g. msdeploy -verb:sync -source:package=<path to zip> -dest:auto -setParamFile=<path to SetParameters.xml>), the parameters inside the ‘SetParameters.xml‘ and ‘Parameters.xml‘ (that is in the zip file) are matched on the parameter name; the ‘Parameters.xml‘ specifies how to locate the setting in ‘web.config’, and the value will be drawn from ‘SetParameters.xml‘. The deployment will yield the following ‘web.config‘:

<?xml version="1.0" encoding="utf-8"?>
<!-- File: Transformed web.config after deployment -->
<configuration>
  <appSettings>
      <add key="MySetting" value="#{MySettingValue}#" />
  </appSettings> 
  <system.web>
     <compilation debug="#{CompilationDebugValue}#" targetFramework="4.8" />
     <httpRuntime targetFramework="4.8" />
   </system.web>
</configuration>

This is not what we want, but the approach of using a ‘Parameters.xml‘ opens up for the next step to achieve parameterization.

Variable substitution

The second, and final step is to add to the pipeline a task that will perform variable substitution. During the deployment, the pipeline artifacts will automatically be downloaded, and in the downloaded artifact, tokens can be substituted:

- task: qetza.replacetokens.replacetokens-task.replacetokens@5
  displayName: Replace tokens in SetParameters.xml
  inputs:
    rootDirectory: <folder SetParameters.xml>
    targetFiles: SetParameters.xml
    actionOnMissing: fail
    actionOnNoFiles: fail

This pipeline task will match any tokenized values in ‘SetParameters.xml’ against pipeline variables with the same name, e.g. the tokenized value #{MySettingValue}# will be replaced by the value of the pipeline variable ‘MySettingValue’. The #{funny syntax}# is the task’s default token pattern, which can be be customized.

Lastly, perform the deployment and specify the location of ‘SetParameters.xml‘, which now contains substituted values:

    - task: IISWebAppDeploymentOnMachineGroup@0
      displayName: Deploy IIS web application <name of web application>
      inputs:
        WebSiteName: <application name>
        Package: <package>
        SetParametersFile: "<path to SetParameters.xml>"

Resources

Blog by Vishal Joshi

Blog by Alan Burn

Blog by Bas Lijten