Blog
Config file transforms with Azure Devops

Config file transforms with Azure Devops

For a long time now our primary CI setup has been based around Team City and Octopus deploy, but as reliable as it is there are things I don't like about it:

  1. It's not a SASS setup meaning there's a VM to occassionaly think about and updates to install. While Octopus is now availiable as a SASS option, Team City is not and moving Octopus will only solve half the problem.
  2. That VM they both sit on every so often gets and issue with it's hard disk being full.
  3. It's complicated to recommend the same setup to clients. You end up having to go through multiple things they need to buy which then require some installation and ongoing maintenance. Ideally we would have a setup thats easy for them to replicate and own themselves with minimal maintenance.

So when we took over a site recently that typically came with no existing CI setup in place, I decided to take a look at using Azure Devops instead. You can use Azure Devops with Octopus Deploy but as it claims to be able to manage releases as well as builds we went for doing the whole thing just in Azure Devops.

Getting a build set up was relatively straight forward so I'm going to skip past that bit, but in short we ended up with a build that will create a web deploy package and publish it as an artifact. Typical msbuild type stuff.

File transforms and variable substitution

The first real tricky point came with replacing variables in config files during a release to each envrionment. We were using the IIS Web App Deploy task to deploy the application to IIS on a VM (no new Azure Web App Services in this setup :( as I said we took over the site and this was just to get automated deploys of what they already have). A simple starting point with this is some built in functionality for XML Variable Substituion in the IIS Web App Deploy task.

Quite simply you can add all your varibles to the variable list, set the scope for which envrionment you want it to apply to and the during the deploy they are replaced in your config. Unlike some tag replacement tools I've used in the past this one actually uses the name of the connecting string or app setting you need to set, so if you need to set a connection string named web, the variable name will be web.

This is also where my problem stated. The description for what XML variable substitution does is:

This was a Sitecore solution and for Sitecore most of your config settings are in Sitecores own Sitecore section of the config file. So in other words the connection string will get updated but the rest won't.

Parameter and SetParameter XML files

My next issue was trying to find a solution is actually quite hard. Searching for this problem either gave me a lot of results for setups using ARM templates (as I said, this was a solution we took over and that kind of change is not on the agenda), or you just get the easy bit above. Searching for Sitecore and Azure Devops also leads you to a lot of results on a cloud infrastructure setup (again not what we're doing here, at least in the short term). Everything that was coming up felt far more complicated than the solution should be.

However the documentation on the XML variable substitution did have one interesting sentance.

A parameters.xml file isn't something I've used before which makes this sentance a bit cryptic. The first half says I can do what I want with an xml file, but the second half says I'll need something else to actually do it.

After a bit of research this all comes back to web deploy. When you do a build that outputs a web deploy package, you get 5 files.

A zip file containing the actual site, a command file which has the script to do the deploy and a set parameters file which is used to set config variables during the deploy. The others aren't so imporant.

To have different config set on different envrionments you just need to edit the set parameters file. But first you need to have the parameter in the set parameters file so that you can actually change it and this is where the parameters.xml file comes in.

Creating the parameter files

Add a file called parameters.xml file to the root of your project and then add parameters as follows.

1<?xml version="1.0" encoding="utf-8" ?>
2<parameters>
3 <parameter name="DataFolderLocation" defaultvalue="#{dataFolder}">
4 <parameterEntry kind="XmlFile" scope="App_Config\\Include\\Z.Project\\DataFolder\.config$" match="/configuration/sitecore/sc.variable[@name='dataFolder']/patch:attribute/text()" />
5 </parameter>
6</parameters>

Some important parts:

default value - The value that the config setting will get set to

scope - The path to the file containing the setting

match - An XPath expression for find the part of the config file to update

Once you have this the build will start producing a SetParameters.xml file containing the extra parameters.

1<?xml version="1.0" encoding="utf-8"?>
2<parameters>
3 <setParameter name="IIS Web Application Name" value="Default Web Site/SiteCore.Website_deploy" />
4 <setParameter name="DataFolderLocation" value="#{dataFolder}" />
5</parameters>

Note: I've set the value to be something I intend to replace in the release process.

Replacing the tokens

With our SetParameters.xml file now contining all the config we need to update, we need a step in the release process that will replace all the tokens with the correct values.

To do this I used a replaced tokens task https://marketplace.visualstudio.com/items?itemName=qetza.replacetokens

Config options need to be set for:

Root Directory - Path to the folder containing the SetParameters.xml file

Target files - A list of files to have replacements done in. In our case this was SiteCore.Website.SetParameters.xml

Token prefix - The prefix on tokens to be search for. Ours was #{

Token suffix - The suffix to denote the end of a token. Ours was }

Lastly in the IIS Web App Deploy step the SetParameters file needed to be selected and the new variables added to the variable list in Azure Devops. The variable names need to be called the bit between your prefix and suffix. i.e. #{datafolder} would be called datafolder.

If you don't set the variables then the log's will show warning for each one it couldn't find.

12019-09-24T17:17:21.6950466Z ##[section]Starting: Replace tokens in SiteCore.Website.SetParameters.xml
22019-09-24T17:17:23.9831695Z ==============================================================================
32019-09-24T17:17:23.9831783Z Task : Replace Tokens
42019-09-24T17:17:23.9831816Z Description : Replace tokens in files
52019-09-24T17:17:23.9831861Z Version : 3.2.1
62019-09-24T17:17:23.9831891Z Author : Guillaume Rouchon
72019-09-24T17:17:23.9831921Z Help : v3.2.1 - [More Information](https://github.com/qetza/vsts-replacetokens-task#readme)
82019-09-24T17:17:23.9831952Z ==============================================================================
92019-09-24T17:17:27.2703037Z replacing tokens in: C:\azagent\A1\_work\r1\a\PublishBuildArtifacts\SiteCore.Website.SetParameters.xml
102019-09-24T17:17:27.3133832Z ##[warning]variable not found: dataFolder
112019-09-24T17:17:27.3179775Z ##[section]Finishing: Replace tokens in SiteCore.Website.SetParameters.xml

With all this set our config has it's variables configured within Azure Devops for each environment,

Sitecore strange language switching

Sitecore strange language switching

The other day we started experiencing a strange issue on one of our test sites. Pages were starting to error and looking at the logs the errors were happening in view's which hadn't been updated in a long time. Some of these seemed like the code wasn't robust enough to handle when a datasource hadn't been set (the classic object not set to an instance of an object error), but fixing these just resulted in an error in another. It also didn't explain why this suddenly started happening, not to mention the components in question should have all had data.

Then we noticed that on the first view of any page of the site with a new session the site would display fine even though these were server errors happening. Navigating to any other page or refreshing though caused the server error to return.

This led us to look at the differences between the request headers and we had our answer. On all subsequent requests the language context had been changed to some random thing which there is no content for. The site itself is not a multilanguage site and only has content for en. Adding the language code into the URL would force the context language back and the page would work again.

How language works in Sitecore

To understand what is happening it's good to know how languages in Sitecore work.

Sitecore is built as a platform which can server content in multiple languages. By default you start with one language in the editor (en) but are able to add more. You can read my blog post from a few years ago on how to do this here (https://himynameistim.com/2015/06/30/sitecore-adding-languages-for-a-multilingual-site/).

Sitecore will recognise which language should be displayed based on a language code in a URL. e.g. himynameistim.com/en-gb/ for English - Great Britian. This is done through the strip language pipeline which picks up these languages and sets the context language.

Config for the link manager then controls if links are generated with these language codes in the URL or not. On a single language site you would have this set to never resulting in URL's without a language code and the default language will be used.

The flaw in all of this though is the strip language pipeline always runs, even when your pages only have one lauange. The pipeline also doesn't check if the language it finds in the url is set up as a language on the site, so quite a lot of two letter combinations will work and change the language context. When this is changed, it is changed for the users session meaning it is possible for a url to inadvertedly cause the language context to change for a user on the site when the site only has one language.

Disabling Strip Language

As the site in question only has one language the fix is quite simple. For multilingual sites the solution is a bit harder.

For a single language site you can simply turn off the strip language functionality. You can do this using a patch config file as follows:

1<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
2 <sitecore>
3 <settings>
4 <setting name="Languages.AlwaysStripLanguage">
5 <patch:attribute name="value">false</patch:attribute>
6 </setting>
7 </settings>
8 </sitecore>
9</configuration>
Removing port 443 from urls generated by Sitecore

Removing port 443 from urls generated by Sitecore

For as long as I've been working on Sitecore there has been this really annoying issue where setting the link manager to include server url and running under https will cause urls to be generated with the port number included. e.g. https://www.himynameistim.com:443/ which naturally you don't actually want.

To overcome this there are a few methods you can take.

Method 1 - Set the Scheme and Port on you site defenition

This is possibly the smallest change you can make as it's just 2 settings in a config file.

Setting the external port on site node to 80 (yes 80) tricks the link manager code into not appending the port number as it does it for everything other than port 80.

1<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
2 <sitecore>
3 <sites xdt:Transform="Insert">
4 <site name="website">
5 <patch:attribute name="hostName">www.MySite.com</patch:attribute>
6 <patch:attribute name="rootPath">/sitecore/content/MySite</patch:attribute>
7 <patch:attribute name="scheme">https</patch:attribute>
8 <patch:attribute name="externalPort">80</patch:attribute>
9 </site>
10 </sites>
11 </sitecore>
12</configuration>

What I don't like about this method though, is your setting something to be wrong to get something else to come out right. It's all a bit wrong.

Method 2 - Write your own link provider

The second method which I have generally done is to write your own provider which strips the port number off the generated URL.

For this you will need:

1. A patch file to add the provider:

1<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
2 <sitecore>
3 <linkManager defaultProvider="sitecore">
4 <patch:attribute
5 name="defaultProvider"
6 value="CustomLinkProvider" />
7 <providers>
8 <add name="CustomLinkProvider"
9 type="MySite.Services.CustomLinkProvider,
10 MySite"
11 languageEmbedding="never"
12 lowercaseUrls="true"
13 useDisplayName="true"
14 alwaysIncludeServerUrl="true"
15 />
16 </providers>
17 </linkManager>
18 <mediaLibrary>
19 <mediaProvider>
20 <patch:attribute name="type">
21 MySite.Services.NoSslPortMediaProvider, MySite
22 </patch:attribute>
23 </mediaProvider>
24 </mediaLibrary>
25 </sitecore>
26</configuration>

2. A helper method that removes the SSL port

1namespace MySite
2{
3 /// <summary>
4 /// Link Helper is used to remove SSL Port
5 /// </summary>
6 public static class LinkHelper
7 {
8 /// <summary>
9 /// This method removes the 443 port number from url
10 /// </summary>
11 /// <param name="url">The url string being evaluated</param>
12 /// <returns>An updated URL minus 443 port number</returns>
13 public static string RemoveSslPort(string url)
14 {
15 if (string.IsNullOrWhiteSpace(url))
16 {
17 return url;
18 }
19
20 if (url.Contains(":443"))
21 {
22 url = url.Replace(":443", string.Empty);
23 }
24
25 return url;
26 }
27 }
28}

3. The custom link provider which first gets the item URL the regular way and then strips the SSL port

1using Sitecore.Data.Items;
2using Sitecore.Links;
3
4namespace MySite
5{
6 /// <summary>Provide links for resources.</summary>
7 public class CustomLinkProvider : LinkProvider
8 {
9 public override string GetItemUrl(Item item, UrlOptions options)
10 {
11 // Some code which manipulates and exams the item...
12
13 return LinkHelper.RemoveSslPort(base.GetItemUrl(item, options));
14 }
15 }
16}
17

4. The same provider for media

1using Sitecore.Data.Items;
2using Sitecore.Resources.Media;
3
4namespace MySite
5{
6 /// <summary>
7 /// This method removes SSL port number from Media Item URLs
8 /// </summary>
9 public class NoSslPortMediaProvider : MediaProvider
10 {
11 /// <summary>
12 /// Overrides Url mechanism for Media Items
13 /// </summary>
14 /// <param name="item">Sitecore Media Item</param>
15 /// <param name="options">Sitecore Media Url Options object</param>
16 /// <returns>Updated Media Item URL minus 443 port</returns>
17
18 public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
19 {
20 var mediaUrl = base.GetMediaUrl(item, options);
21 return LinkHelper.RemoveSslPort(mediaUrl);
22 }
23 }
24}

What I don't like about this method is it's messy in the opposite way. The port number is still being added, and we're just adding code to try and fix it after.

Credit to Sabo413 for the code in this example

Method 3 - Official Sitecore Patch

Given that it's Sitecore's bug, it does actually make sense that they fix it. After all people are paying a license fee for support! This simplifies your solution down to 1 extra patch file and a dll. What's better is as it's Sitecores code they have the responsibility of fixing it, if it ever breaks something, and you have less custom code in your repo.

You can get the fix here for Sitecore version 8.1 - 9.0.

So this may leave you wondering how did Sitecore fix it? Well having a look inside the dll reveals they wen't for method 2.