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 🙂


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.