Category Archives: Sitecore.Habitat

Continuous integration and deployment of Sitecore Habitat/Helix as an Azure Web App


Hello happy people, don’t we all just love Habitat/Helix. If you don’t know what I’m talking about(shame on you), please read about it here:
Sitecore Helix Documentation
Sitecore Habitat – good demo with code and all
Do a Helix Technical Workshop at Sitecore

Sitecore is now available as an Azure web app. This is nothing new but people are finally seeing it as the default option, which is great 😄

Rob Habraken and Bas Lijten have some really good posts regarding Sitecore Deployments on Azure:
Blue Green Sitecore Deployments on Azure (Rob Habraken)
Sitecore 8.2-1 on Azure Web Apps (Rob Habraken)
Zero downtime deployments with Sitecore on Azure (Bas Lijten)
Use the Sitecore Azure toolkit to deploy your on premises environment (Bas Lijten)

Ok lets move on… In this post I would like to share with you guys how to setup a Habitat/Helix solution as an Azure web app, where unicorn will be our deliverer/savior 😉
Let me explain:
We will setup a clean Sitecore website in Azure.
Next will be to change the ci(continuous integration) gulp file, for copying and transforming files to a deploy folder.
Instead of using (item) packages we will use Unicorn as our “item deliverer”.
It will all be deployed to the azure web app instance with good old ftp(this is not the best approach but I just want to show you how easy it could be)
Everything will run in TeamCity or Visual Studio Team Services.

As always let’s divide our work into following steps:
1. Setup a clean Sitecore instance in Azure – Azure web app.
2. Setup folders and config files to transform on.
3. Prepare and add transformations for the continuous integration script.
4. Update the continuous integration script file – gulpfile-ci.js.
5. TeamCity – Add build steps.
6. Visual Studio Team Services – Add build tasks.

1. Setup a clean Sitecore instance in Azure – Azure web app

There are a lot of good posts on how you to setup Sitecore as an Azure web app.
Sitecore Azure Toolkit
Deploy a new Sitecore environment to Azure App Service
And really good posts from the Sitecore community:
An introduction to Sitecore on Azure Web Apps
Sitecore 8.2 update 1: Azure deployments, ARM, Web Deploy and the Sitecore Azure Toolkit
Sitecore on Azure: Create custom web deploy packages using the Sitecore Azure Toolkit

Let the posts guide you 🙂

2. Setup folders and config files to transform on.

Instead of publishing and transforming files to the website folder, we will do that in a Deploy folder(Which will be placed in the root of the solution).
We will also have a folder for configs to copy from – ConfigsToBuildFrom(It will be placed in the root of the solution), the folder will contain the following files:

  • Website
    • Web.config
    • App_Config
      • Include
        • DataFolderUnicornMaster.config
        • UnicornData.config
    • Security
      • Domains.config
  • WebsiteCD
    • Web.config

Here we place the web.config’s (from the Azure CM instance and the Azure CD instance), some unicorn configs and the Domain.config.
UnicornData.config sets the sourceFolder:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <sc.variable name="sourceFolder" value="$(dataFolder)\unicorn" />
  </sitecore>
</configuration>

DataFolderUnicornMaster.config holds the path to the source of the serlialization files

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <unicorn>
      <defaults>
        <targetDataStore physicalRootPath="$(dataFolder)\Unicorn\$(configurationName)" useDataCache="false" type="Rainbow.Storage.SerializationFileSystemDataStore, Rainbow" singleInstance="true" xdt:Transform="Replace" xdt:Locator="Match(type)"/>
      </defaults>
    </unicorn>
  </sitecore>
</configuration>

3. Prepare and add transformations for the continuous integration gulp script.

Next will be to change(and add) transforming files. Instead of one transforming “type” we will have one per role. Here we have updated/changed the domain.config.transform file(from the habitat project) and the web.config.transform file(from the common project):
domain.config.local.transform
domain.config.cm.transform
domain.config.cd.transform

web.config.local.transform
web.config.cm.transform
web.config.cd.transform

Then in the gulpfile-ci.js(continuous integration script) we will have one transforming task(method) per build “role”.

We also need to transform some of the config patch files. In Foundation.Indexing we need to change the Foundation.Indexing.config file(right now its using the lucene index configuration). We will create two new transforms:
– Foundation.Indexing.config.cm.transform
– Foundation.Indexing.config.cd.transform.
They will replace the lucene index configuration with the cloud index configuration:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration>
          <add xdt:Transform="Remove" />
        </defaultLuceneIndexConfiguration>
        <defaultCloudIndexConfiguration type="Sitecore.ContentSearch.Azure.CloudIndexConfiguration, Sitecore.ContentSearch.Azure" xdt:Transform="InsertIfMissing">
          <fieldMap type="Sitecore.ContentSearch.Azure.FieldMaps.CloudFieldMap, Sitecore.ContentSearch.Azure" >
            <fieldNames hint="raw:AddFieldByFieldName">
              <field fieldName="all_templates" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Collections.Generic.List`1[[System.String, mscorlib]]" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" >
                <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <field fieldName="has_presentation" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure"  />
              <field fieldName="has_search_result_formatter" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure"  />
              <field fieldName="search_result_formatter" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" type="System.String" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" />
            </fieldNames>
          </fieldMap>
          <virtualFields type="Sitecore.ContentSearch.VirtualFieldProcessorMap, Sitecore.ContentSearch">
            <processors hint="raw:AddFromConfiguration">
              <add fieldName="content_type" type="Alite.Foundation.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Alite.Foundation.Indexing"/>
            </processors>
          </virtualFields>
          <documentOptions type="Sitecore.ContentSearch.Azure.CloudSearchDocumentBuilderOptions,Sitecore.ContentSearch.Azure" >
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="has_presentation" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.HasPresentationComputedField, Sitecore.Foundation.Indexing</field>
              <field fieldName="all_templates" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.AllTemplatesComputedField, Sitecore.Foundation.Indexing</field>
              <field fieldName="has_search_result_formatter" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.HasSearchResultFormatterComputedField, Sitecore.Foundation.Indexing</field>
              <field fieldName="search_result_formatter" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Sitecore.Foundation.Indexing</field>
            </fields>
          </documentOptions>
        </defaultCloudIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>

In Foundation.LocalDatasource we need to change the Foundation.LocalDatasource.config file(right now its using the lucene index configuration).
We will create two new transforms:
– Foundation.LocalDatasource.config.cm.transform
– Foundation.LocalDatasource.config.cd.transform.
They will replace the lucene index configuration with the cloud index configuration:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration xdt:Transform="Remove" />
        <defaultCloudIndexConfiguration type="Sitecore.ContentSearch.Azure.CloudIndexConfiguration, Sitecore.ContentSearch.Azure" xdt:Transform="InsertIfMissing">
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="local_datasource_content" storageType="NO" indexType="TOKENIZED">Sitecore.Foundation.LocalDatasource.Infrastructure.Indexing.LocalDatasourceContentField, Sitecore.Foundation.LocalDatasource</field>
            </fields>
          </documentOptions>
        </defaultCloudIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>

In Common.Website we need to change the Common.Website.config file.
We will create two new transforms:
– Common.Website.config.cm.transform
– Common.Website.config.cd.transform
They will replace the rootHostName, Analytics.CookieDomain and (if you want) the Analytics.ClusterName:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <sitecore>
      <sc.variable name="rootHostName" value="yourdomain" xdt:Transform="SetAttributes(value)" xdt:Locator="Match(name)"/>
      <settings>
        <setting name="Analytics.CookieDomain" value="yourdomain" xdt:Transform="SetAttributes(value)" xdt:Locator="Match(name)"/>
        <setting name="Analytics.ClusterName" value="yourdomain" xdt:Transform="SetAttributes(value)" xdt:Locator="Match(name)"/>
      </settings>
    </sitecore>
</configuration>

Finally in Habitat.Website we need to change the Habitat.Website.config file.
We will create two new transforms:
– Habitat.Website.config.cm.transform
– Habitat.Website.config.cd.transform

In Habitat.Website.config.cm.transform we will just change the target host name:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <sites>
      <site name="habitat" targetHostName="yourSiteName.$(rootHostName)" xdt:Transform="SetAttributes(targetHostName)" xdt:Locator="Match(name)"/>
    </sites>
  </sitecore>
</configuration>

And in Habitat.Website.config.cd.transform we will follow jammykam’s advice from his great post – Disable Edit and Preview Modes on your CD servers:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <sites>
      <site name="habitat" targetHostName="yourSiteName.$(rootHostName)" enablePreview="false" enableWebEdit="false" enableDebugger="false" xdt:Transform="SetAttributes(targetHostName,enablePreview,enableWebEdit,enableDebugger)" xdt:Locator="Match(name)" />
    </sites>
  </sitecore>
</configuration>

Instead of transforming you can go for Patch Override, there is a great post from jammykam – Helix: Patching your Config Patches for Specific Environments

4. Update the continuous integration script file – gulpfile-ci.js

So now we have setup the config and transformation files. Next will be to update our gulpfile-ci.js file, which will be used in TeamCity or Visual Studio Team Services.

We will add some new variables to the script, by pointing out paths to the new folders. (By default the “data folder” is the App_Data folder in a Sitecore Azure web app)

var webTransformationsToBuildFrom = path.resolve("./ConfigsToBuildFrom/Website");
var webCDTransformationsToBuildFrom = path.resolve("./ConfigsToBuildFrom/WebsiteCD");
var dataFolder = path.resolve("./Deploy/Data/App_Data");
var tempWebsite = path.resolve("./Deploy/Website");
var tempWebsiteCD = path.resolve("./Deploy/WebsiteCD");
config.websiteRoot = tempWebsite;

Here is the main task – CI-And-Prepare-Files-CM-CD:

gulp.task("CI-And-Prepare-Files-CM-CD", function (callback) {
    runSequence(
        "CI-Clean",
        "CI-Copy-Configs-CM",
        "CI-Publish",
        "CI-Copy-Website-CD",
        "CI-Copy-Configs-CD",
        "CI-Prepare-Files-CM",
        "CI-Prepare-Files-CD",
        "CI-Apply-Xml-Transform-CM",
        "CI-Apply-Xml-Transform-CD",
        "CI-Copy-Items-For-Unicorn",
        callback);
});

Let’s go trough the different tasks…
In CI-Clean we just clean the deploy folder.

gulp.task("CI-Clean", function (callback) {
    rimrafDir.sync(path.resolve("./Deploy"));
    callback();
});

CI-Copy-Configs-CM, will copy files from the ConfigsToBuildFrom folder to the Deploy folder.

gulp.task("CI-Copy-Configs-CM", function () {
    return gulp.src(webTransformationsToBuildFrom + "/**")
        .pipe(gulp.dest(tempWebsite));
});

CI-Publish, will publish all your projects to the Deploy/Website folder.

gulp.task("CI-Publish", function (callback) {
    config.buildConfiguration = "Release";
    runSequence(
        "Nuget-Restore",
        "Build-Solution",
        "Publish-Foundation-Projects",
        "Publish-Feature-Projects",
        "Publish-Project-Projects", callback);
});

CI-Copy-Website-CD, will copy all the files from Deploy/Website to Deploy/WebsiteCD.

gulp.task("CI-Copy-Website-CD", function () {
    return gulp.src(tempWebsite + "/**")
        .pipe(gulp.dest(tempWebsiteCD));
});

CI-Prepare-Files-CM, will go through and remove unwanted files and dll’s for our CM instance. I got the idea from this great post – Sitecore Habitat Deployment.

gulp.task("CI-Prepare-Files-CM", function (callback) {
    var excludeList = [
        tempWebsite + "\\bin\\{Sitecore,Lucene,Newtonsoft,System,Microsoft.Web.Infrastructure}*dll",
        tempWebsite + "\\bin\\*.pdb",
        tempWebsite + "\\compilerconfig.json.defaults",
        tempWebsite + "\\packages.config",
        tempWebsite + "\\App_Config\\Include\\{Feature,Foundation,Project}\\z.*DevSettings.config",
        tempWebsite + "\\App_Data\\*",
        "!" + tempWebsite + "\\bin\\Sitecore.Support*dll",
        "!" + tempWebsite + "\\bin\\Sitecore.{Feature,Foundation,Project}*dll",
        tempWebsite + "\\bin\\{Sitecore.Foundation.Installer}*",
        tempWebsite + "\\App_Config\\Include\\Foundation\\Foundation.Installer.config",
        tempWebsite + "\\README.md",
        tempWebsite + "\\bin\\HtmlAgilityPack*dll",
        tempWebsite + "\\bin\\ICSharpCode.SharpZipLib*dll",
        tempWebsite + "\\bin\\Microsoft.Extensions.DependencyInjection*dll",
        tempWebsite + "\\bin\\MongoDB.Driver*dll",
        tempWebsite + "\\bin\\Microsoft.Web.XmlTransform*dll"
    ];
    console.log(excludeList);

    return gulp.src(excludeList, { read: false }).pipe(rimraf({ force: true }));
});

Notice that we are not removing the serialization.config’s from the CM instance, we will use them for some Unicorn magic later 🙂

CI-Prepare-Files-CD, will go through and remove unwanted files and dll’s for our CD instance. I got the idea from this great post – Sitecore Habitat Deployment.

gulp.task("CI-Prepare-Files-CD", function (callback) {
   var excludeList = [
        tempWebsiteCD + "\\bin\\{Sitecore,Lucene,Newtonsoft,System,Microsoft.Web.Infrastructure}*dll",
        tempWebsiteCD + "\\bin\\*.pdb",
        tempWebsiteCD + "\\compilerconfig.json.defaults",
        tempWebsiteCD + "\\packages.config",
        tempWebsiteCD + "\\App_Config\\Include\\{Feature,Foundation,Project}\\z.*DevSettings.config",
        tempWebsiteCD + "\\App_Data\\*",
        "!" + tempWebsiteCD + "\\bin\\Sitecore.Support*dll",
        "!" + tempWebsiteCD + "\\bin\\Sitecore.{Feature,Foundation,Project}*dll",
        tempWebsiteCD + "\\bin\\{Sitecore.Foundation.Installer}*",
        tempWebsiteCD + "\\App_Config\\Include\\Rainbow",
        tempWebsiteCD + "\\App_Config\\Include\\Unicorn",
        tempWebsiteCD + "\\App_Config\\Include\\Rainbow*.config",
        tempWebsiteCD + "\\App_Config\\Include\\Unicorn*.config",
        tempWebsiteCD + "\\App_Config\\Include\\Foundation\\*.Serialization.config",
        tempWebsiteCD + "\\App_Config\\Include\\Feature\\*.Serialization.config",
        tempWebsiteCD + "\\App_Config\\Include\\Project\\*.Serialization.config",
        tempWebsiteCD + "\\App_Config\\Include\\DataFolderUnicornMaster.config",
        tempWebsiteCD + "\\App_Config\\Include\\Foundation\\Foundation.Installer.config",
        tempWebsiteCD + "\\README.md",
        tempWebsiteCD + "\\bin\\HtmlAgilityPack*dll",
        tempWebsiteCD + "\\bin\\ICSharpCode.SharpZipLib*dll",
        tempWebsiteCD + "\\bin\\Microsoft.Extensions.DependencyInjection*dll",
        tempWebsiteCD + "\\bin\\MongoDB.Driver*dll",
        tempWebsiteCD + "\\bin\\Microsoft.Web.XmlTransform*dll",
        tempWebsiteCD + "\\bin\\Rainbow*dll",
        tempWebsiteCD + "\\bin\\Unicorn*dll",
        tempWebsiteCD + "\\bin\\Kamsar.WebConsole*dll"
    ];
    console.log(excludeList);

    return gulp.src(excludeList, { read: false }).pipe(rimraf({ force: true }));
});

Here we will remove everything that has to do with Unicorn.

CI-Apply-Xml-Transform-CM, will run all the CM transformations.

gulp.task("CI-Apply-Xml-Transform-CM", function () {
     var layerPathFilters = ["./src/Foundation/**/*.CM.transform", "./src/Feature/**/*.CM.transform", "./src/Project/**/*.CM.transform", "!./src/**/obj/**/*.CM.transform", "!./src/**/bin/**/*.CM.transform"];
    return gulp.src(layerPathFilters)
        .pipe(foreach(function (stream, file) {

            var fileToTransform = file.path.replace(/.+code\\(.+)\.CM.transform/, "$1");
            util.log("Applying configuration transform: " + file.path);
            return gulp.src("./scripts/applytransform.targets")
                .pipe(msbuild({
                    targets: ["ApplyTransform"],
                    configuration: "Release",
                    logCommand: false,
                    verbosity: "minimal",
                    stdout: true,
                    errorOnFail: true,
                    maxcpucount: 0,
                    toolsVersion: config.buildToolsVersion,
                    properties: {
                        Platform: config.buildPlatform,
                        WebConfigToTransform: tempWebsite,
                        TransformFile: file.path,
                        FileToTransform: fileToTransform
                    }
                }));
        }));
});

CI-Apply-Xml-Transform-CD, will run all the CD transformations.

gulp.task("CI-Apply-Xml-Transform-CD", function () {
     var layerPathFilters = ["./src/Foundation/**/*.CD.transform", "./src/Feature/**/*.CD.transform", "./src/Project/**/*.CD.transform", "!./src/**/obj/**/*.CD.transform", "!./src/**/bin/**/*.CD.transform"];
    return gulp.src(layerPathFilters)
        .pipe(foreach(function (stream, file) {

            var fileToTransform = file.path.replace(/.+code\\(.+)\.CD.transform/, "$1");
            util.log("Applying configuration transform: " + file.path);
            return gulp.src("./scripts/applytransform.targets")
                .pipe(msbuild({
                    targets: ["ApplyTransform"],
                    configuration: "Release",
                    logCommand: false,
                    verbosity: "minimal",
                    stdout: true,
                    errorOnFail: true,
                    maxcpucount: 0,
                    toolsVersion: config.buildToolsVersion,
                    properties: {
                        Platform: config.buildPlatform,
                        WebConfigToTransform: tempWebsiteCD,
                        TransformFile: file.path,
                        FileToTransform: fileToTransform
                    }
                }));
        }));
});

CI-Copy-Items-For-Unicorn, will grab all the serialized items(yml files) and put them in following path – Deploy/Data/App_Data/Unicorn(for our CM instance).

gulp.task("CI-Copy-Items-For-Unicorn", function () {
    return gulp.src("./src/**/serialization/**/*.yml")
        .pipe(gulp.dest(dataFolder + "/Unicorn/"));
});

CI-Sync-Unicorn-CM. will be called from TeamCity or Visual Studio Team Services to sync the Sitecore items.

gulp.task("CI-Sync-Unicorn-CM", function (callback) {
    var options = {};
    options.siteHostName = habitat.getStageUrl();
    options.authenticationConfigFile = config.websiteRoot + "/App_config/Include/Unicorn.SharedSecret.config";

    unicorn(function () { return callback() }, options);
});

When task CI-And-Prepare-Files-CM-CD is done, we should have a Deploy folder ready (for deployment):

To make it even better, next step could be to make a custom web deploy package (using the Sitecore Azure Toolkit).
Check out Sitecore on Azure: Create custom web deploy packages using the Sitecore Azure Toolkit from Bas Lijten

5. TeamCity – Add build steps

The next move is to automate our deployment, I was thinking of using TeamCity and Visual Studio Team Services.

Let’s start with TeamCity. I will not explain on how to setup TeamCity, but it’s quite straight forward. You can read all about it here – Installing and Configuring the TeamCity Server

I added some plugins in order to run the xunit tests and execute the gulp tasks:
xUnit-TeamCity
TeamCity.Node (gulp)

Next we will create the build steps:
1. Nuget Restore
NuGet Installer
Solution: MySolution.sln

2. Build solution
Visual Studio (sln)
Build file path: MySolution.sln
Targets: Rebuild
Configuration: Release

3. Run all tests
xUnit
Version 2.1.0 (AnyCPU/MSIL/.NET 4.5)
Included assemblies: **\bin\debug*Test*.dll

4. npm install
Command Line
Custom script: npm install

5. Generate deploy files for Azure
Gulp
Run targets: CI-And-Prepare-Files-For-CM-CD

6. Upload files To Azure CM
FTP Upload
Target FTP server: ftp://xxxx.ftp.azurewebsites.windows.net/site/wwwroot/
Source folder: %teamcity.build.workingDir%/Deploy/Website/**

7. Upload files To Azure CD
FTP Upload
Target FTP server: ftp://xxxx.ftp.azurewebsites.windows.net/site/wwwroot/
Source folder: %teamcity.build.workingDir%/Deploy/WebsiteCD/**

8. Copy Unicorn files
FTP Upload
Target FTP server: ftp://xxxx.ftp.azurewebsites.windows.net/site/wwwroot/
Source folder: %teamcity.build.workingDir%/Deploy/Data/**

9. Sync items with Unicorn
Gulp
Run targets: CI-Sync-Unicorn-CM

In step 8 and 9 that’s where all the magic happens 🙂
We will upload all the serialized Sitecore items and thanks to the serialization.config’s(which we didn’t remove) we can now call task CI-Sync-Unicorn-CM and sync all the serialized sitecore items to our Azure CM instance – love it !

6. Visual Studio Team Services – Add tasks

Visual Studio Team Services is a great service and it’s easy to get started with. The best of it all – it’s all in the cloud. Read all about it here – I want to to do it now!!

Here are our created build tasks:

Finally we need to setup a Trigger, we don’t want to start the build manually. In this case we will listen to the master branch from our Bitbucket repository:

That’s it guys, I must say that I fell in love with Visual Studio Team Services – Put it all in the cloud !!

That’s all for now folks 🙂

Advertisements

Real-time in Sitecore.Habitat with SignalR and MongoDB

RealTimeYo

Personalizing content has always been a big fascination of mine and I just love Sitecore’s very elegant solution by using the rules engine. Thanks to this very cool tool the editors can do the following:

Rule-based personalization
Rule-based personalization uses logic-based rules to determine the content that is displayed on a webpage. For example, you can set rules based on the IP address or physical location of your visitors, the keywords they use to reach your site, their mobile device, or the goals that they achieve on your website to determine the content that is displayed.

Adaptive personalization

Adaptive personalization is a feature that dynamically changes the content of your website based on the visitor’s behavior during a visit. Adaptive personalization uses visitor profiles and pattern-card matching to dynamically adapt the content shown to visitors in real time. You can set adaptive personalization rules in the Rules Set Editor.

Historical personalization
You can use rules that personalize content based on a contact’s historical or past behavior, rather than their actions from the current session. The Key Behavior Cache contains information about a contact’s recent activities across all channels. The Contact Behavior Profile contains information about a contact’s past profile matches.

You can create and implement personalization rules based on the information in the Key Behavior Cache and Contact Behavior Profile, enabling you to provide contacts with content that is relevant based on their past behavior, rather than just their current interaction with your brand.

We just need one more thing to make the personalization even more interesting – Personalization in real-time!
How can we do that? By using SignalR of course 🙂 (There are of course other ways to do this but SignalR has a special place in my heart)

There are a number of Sitecore “frameworks” out there but there is one who stands out – Sitecore.Habitat. Thanks to the Modular Architecture approach everything is clean. Anders Laub made a great post about this – The groundbreaking Sitecore Habitat

If you guys are going to the Sitecore Symposium 2016 New Orleans, then you must attend on the Habitat Master Class. The course is held by mr Habitat himself – Thomas Eldblom 🙂

First we need to setup the SignalR and hook it up with MongoDB, that is what this post will be about. For some time ago I did a post about real-time in Sitecore, Real-time in Sitecore using SignalR and MongoDB. I’ve decided to migrate the old code to the best ever Sitecore “framework” – Sitecore.Habitat.

Here is how the project look like after the migration. It’s a typical “Foundation” project, since it probably will be used by “Feature” projects.
I’ve highlighted the ones that I will take up in the post.
Realtime für die Leute
Check it out on the GitHub – GoranHalvarsson/Habitat

web.config.transform

In order to make SignalR work properly we need to do some changes/updates in webconfig.
This guy was indeed tricky, working with Xdt transform is not easy but thanks to this little puppy it made my life so much easier – Web.config Transformation Syntax for Web Project Deployment Using Visual Studio.

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">

  <appSettings xdt:Transform="InsertIfMissing">
    <add key="ValidationSettings:UnobtrusiveValidationMode" value="None" xdt:Transform="InsertIfMissing" xdt:Locator="Match(key)"/>
  </appSettings>
  <system.web>
	<httpRuntime targetFramework="4.5" requestValidationMode="2.0" xdt:Transform="SetAttributes(targetFramework,requestValidationMode)" />
  </system.web>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='MongoDB.Bson')">
        <assemblyIdentity name="MongoDB.Bson" publicKeyToken="f686731cfb9cc103" culture="neutral" />
        <codeBase version="2.2.4.26" href="bin\MongoDB.Bson.dll" />
        <codeBase version="1.10.0.62" href="bin\Sitecore\MongoDB.Bson.dll" />
      </dependentAssembly>
      <dependentAssembly xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='MongoDB.Driver')">
        <assemblyIdentity name="MongoDB.Driver" publicKeyToken="f686731cfb9cc103" culture="neutral" />
        <codeBase version="2.2.4.26" href="bin\MongoDB.Driver.dll" />
        <codeBase version="1.10.0.62" href="bin\Sitecore\MongoDB.Driver.dll" />
      </dependentAssembly>
      <dependentAssembly  xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='Microsoft.AspNet.SignalR.Core')">
        <assemblyIdentity name="Microsoft.AspNet.SignalR.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-2.2.0.0" newVersion="2.2.0.0" />
      </dependentAssembly>
      <dependentAssembly xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='Microsoft.Owin')">
        <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='Microsoft.Owin.Security')">
        <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly xdt:Transform="Replace" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='Newtonsoft.Json')">
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" />
        <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>

</configuration>

Lets break it down, we will start with the changes to make SignalR work properly.

The request validation feature in ASP.NET provides a certain level of default protection against cross-site scripting (XSS) attacks. In ASP.NET 4, by default, request validation is enabled for all requests. As a result, request validation errors might now occur for requests that previously did not trigger errors. To revert to the behavior of the ASP.NET 2.0 request validation feature we need to add requestValidationMode=”2.0″ in the httpRuntime.

To prevent the Websocket error, 500, which means that the server does not support websockets. In this specific case server means the server (IIS) supports WebSockets but ASP.NET version of your application which hosts SignalR does not support websockets. In the httpRuntime set targetframework to 4.5.

Finally the UnobtrusiveValidationMode setting is really not necessary since this is for webforms only, but hey lets put that one there too 🙂

  <appSettings xdt:Transform="InsertIfMissing">
    <add key="ValidationSettings:UnobtrusiveValidationMode" value="None" xdt:Transform="InsertIfMissing" xdt:Locator="Match(key)"/>
  </appSettings>
  <system.web>
	<httpRuntime targetFramework="4.5" requestValidationMode="2.0" xdt:Transform="SetAttributes(targetFramework,requestValidationMode)" />
  </system.web>

Next part is the dll’s. Since I’m using the 2.0 .Net Driver for MongoDB with Async support but Sitecore is using the previous version of the driver. We need to do the following in config:

  <dependentAssembly xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='MongoDB.Bson')">
    <assemblyIdentity name="MongoDB.Bson" publicKeyToken="f686731cfb9cc103" culture="neutral" />
    <codeBase version="2.2.4.26" href="bin\MongoDB.Bson.dll" />
    <codeBase version="1.10.0.62" href="bin\Sitecore\MongoDB.Bson.dll" />
  </dependentAssembly>
  <dependentAssembly xdt:Transform="InsertIfMissing" xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='MongoDB.Driver')">
    <assemblyIdentity name="MongoDB.Driver" publicKeyToken="f686731cfb9cc103" culture="neutral" />
    <codeBase version="2.2.4.26" href="bin\MongoDB.Driver.dll" />
    <codeBase version="1.10.0.62" href="bin\Sitecore\MongoDB.Driver.dll" />
  </dependentAssembly>

You will also have to create a Sitecore folder in your bin folder in the project, here you will place the “old” dll’s that Sitecore is using – MongoDB.Bson.dll and MongoDB.Driver.dll.

There will also be some gulp scripts that will copy over the specific dll’s when it’s time to publish to the website:

gulp.task("Realtime-Copy-MongoDB-dll's", function () {
    console.log("Copying mongodb assemblies to website");
    var root = "./src/Foundation/Realtime/code/bin/Sitecore";
    var binFiles = root + "/*.{dll,pdb}";
    var destination = config.websiteRoot + "/bin/Sitecore/";
   
    console.log("copying to " + destination);


    return gulp.src(binFiles, { base: root })
      .pipe(gulp.dest(destination));
});

gulp.task("Realtime-Copy-Newtonsoft-dll", function () {
    console.log("Copying newtonsoft assemblies to website");
    var root = "./src/Foundation/Realtime/code/bin";
    var binFiles = root + "/Newtonsoft.Json.{dll,pdb,xml}";
    var destination = config.websiteRoot + "/bin/";

    console.log("copying to " + destination);


    return gulp.src(binFiles, { base: root })
      .pipe(gulp.dest(destination));
});

gulp.task("Realtime-Xml-Transform", function () {
    return gulp.src("./src/Foundation/Realtime/**/code/*.csproj")
      .pipe(foreach(function (stream, file) {
          return stream
            .pipe(debug({ title: "Applying transform realtime:" }))
            .pipe(msbuild({
                targets: ["ApplyTransform"],
                configuration: config.buildConfiguration,
                logCommand: false,
                verbosity: "normal",
                maxcpucount: 0,
                toolsVersion: 14.0,
                properties: {
                    WebConfigToTransform: config.websiteRoot + "\\web.config"
                }
            }));
      }));

});

The last one is to make sure that we run the xml transform for the project.

RealtimeHub.cs

This is the brain of the SignalR, the hub will take care of incoming(javascript calls from the client) and outgoing(calling javascript methods on the client) calls.

It’s very well described in my older post: Create a real-time connection.
You can check out the the code on GitHub – RealtimeHub.cs

Realtime.js

This is the heart of the SignalR, the javascript will feed and listen to the “brain”(hub).

It’s also very well described in my older post: Gather visitor data.
You can check out the the code on GitHub – Realtime.js

RegisterSignalrProcessor.cs

The pipeline will replace the old startup class. We will need to mark it with OwinStartup(SignalR requires Owin).

using Microsoft.Owin;
using VisionsInCode.Foundation.Realtime.Pipelines;

[assembly: OwinStartup(typeof(RegisterSignalrProcessor))]
namespace VisionsInCode.Foundation.Realtime.Pipelines
{
  using Microsoft.AspNet.SignalR;
  using Owin;
  using Sitecore.Diagnostics;
  using Sitecore.Pipelines;
  using VisionsInCode.Foundation.Realtime.Infrastructure;
  using VisionsInCode.Foundation.Realtime.Repositories;

  public class RegisterSignalrProcessor
  {
    public void Configuration(IAppBuilder app)
    {
      Log.Info("OwinStartup has started", this);

      
      GlobalHost.HubPipeline.AddModule(new ErrorHandlingHubPipelineModule());


      GlobalHost.DependencyResolver.Register(
            typeof(RealtimeHub),
            () => new RealtimeHub(new RealtimeVisitorRepository(), new GeoCoordinateRepository(), new HubContextService(), new GeocoderService()));

      app.MapSignalR();

    }

    public virtual void Process(PipelineArgs args)
    {
      Log.Info("Pipeline RegisterSignalrProcessor called", this);

    }

  }
}

You can check out the the code on GitHub – RegisterSignalrProcessor.cs

Foundation.Realtime.config

And finally the config patch file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <settings>
      <setting name="IgnoreUrlPrefixes">
        <patch:attribute name="value">/sitecore/default.aspx|/trace.axd|/webresource.axd|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.DialogHandler.aspx|/sitecore/shell/applications/content manager/telerik.web.ui.dialoghandler.aspx|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.SpellCheckHandler.axd|/Telerik.Web.UI.WebResource.axd|/sitecore/admin/upgrade/|/layouts/testing|/sitecore/service/xdb/disabled.aspx|/signalr|/signalr/hubs</patch:attribute>
      </setting>
    </settings>
    <pipelines>
      <initialize>
        <processor type="VisionsInCode.Foundation.Realtime.Pipelines.RegisterSignalrProcessor, VisionsInCode.Foundation.Realtime"
       patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" ></processor>
      </initialize>
      <mvc.getPageRendering>
        <processor
          patch:before="*[@type='Sitecore.Mvc.Pipelines.Response.GetPageRendering.GetLayoutRendering, Sitecore.Mvc']"
          type="Sitecore.Foundation.Assets.Pipelines.GetPageRendering.AddAssets, Sitecore.Foundation.Assets">
          <defaultAssets hint="raw:AddAsset">
            <asset type="JavaScript" file="/Scripts/Realtime/jquery.signalR-2.2.1.js" location="Body" />
            <asset type="JavaScript" file="/signalr/hubs" location="Body" />
            <asset type="JavaScript" file="/Scripts/Realtime/ClientTracker.js" location="Body" />
            <asset type="JavaScript" file="/Scripts/Realtime/Realtime.js" location="Body" />
          </defaultAssets>
        </processor>
      </mvc.getPageRendering>
    </pipelines>
  </sitecore>
</configuration>

Lets break it down.
We need to stop Sitecore to capture SignalR requests for that we need to update IgnoreUrlPrefixes by adding signalr|/signalr/hubs:

    <settings>
      <setting name="IgnoreUrlPrefixes">
        <patch:attribute name="value">/sitecore/default.aspx|/trace.axd|/webresource.axd|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.DialogHandler.aspx|/sitecore/shell/applications/content manager/telerik.web.ui.dialoghandler.aspx|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.SpellCheckHandler.axd|/Telerik.Web.UI.WebResource.axd|/sitecore/admin/upgrade/|/layouts/testing|/sitecore/service/xdb/disabled.aspx|/signalr|/signalr/hubs</patch:attribute>
      </setting>
    </settings> 

Next is to place the “startup” pipeline in the correct “call order”:

      <initialize>
        <processor type="VisionsInCode.Foundation.Realtime.Pipelines.RegisterSignalrProcessor, VisionsInCode.Foundation.Realtime"
       patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" ></processor>
      </initialize>

Finally add the javascripts in the correct order.

      <mvc.getPageRendering>
        <processor
          patch:before="*[@type='Sitecore.Mvc.Pipelines.Response.GetPageRendering.GetLayoutRendering, Sitecore.Mvc']"
          type="Sitecore.Foundation.Assets.Pipelines.GetPageRendering.AddAssets, Sitecore.Foundation.Assets">
          <defaultAssets hint="raw:AddAsset">
            <asset type="JavaScript" file="/Scripts/Realtime/jquery.signalR-2.2.1.js" location="Body" />
            <asset type="JavaScript" file="/signalr/hubs" location="Body" />
            <asset type="JavaScript" file="/Scripts/Realtime/ClientTracker.js" location="Body" />
            <asset type="JavaScript" file="/Scripts/Realtime/Realtime.js" location="Body" />
          </defaultAssets>
        </processor>
      </mvc.getPageRendering>

You can check out the the code on GitHub – Foundation.Realtime.config

Ops, I almost forgot we also need to set the connection string to the new mongodb database, signalr, in the connectionStrings.config:

<add name="signalr" connectionString="mongodb://localhost:27017/habitat_local_signalr" />

There will be a second post where we will use SignalR to change content on the fly(in real-time).

That’s all for now folks 🙂