Skip to main content

Sitecore 9 - Implementing Calculated Facets

I am going to implement a calculated facet in this section. It is going to include creating, making calculations, how to debug calculation code and at the end, I am going to show how to display data in experience profile.

But first let's remember the scenario again.

Imagine that you have a website displaying movies. Visitors are able to see movie details and take some actions like save movie or share it. 

You want to follow the visitors' activities and you want to take some marketing actions based on those activities. For example, if a contact visits a movie more than X time or she/he saves a movie, you want to send those movies to an external system. In addition, there is going to be a limit to send same movie. Such as, it will not be possible to send same movie more than 2 times. 

In previous post, we saw that we are able filter and get necessary data using XConnect api.

However, as I mentioned in previous post, I am going to use a calculated facet which will keep all the necessary info for a contact. The reason I want to use a calculated facet that -you will see the details in segmentation rule post- there are some limitations if you want to create a segmentation rule. It will not be possible there to create complex queries. I am going to explain this later, but first, I am going to create my calculated facet and display movies with visit counts in contacts' experience profile.

Here is my calculated facet definition, it keeps a dictionary that the key value is movie id and the value is the visit details.

namespace Playground.XConnect.Facets
{
    [Serializable]
    [FacetKey(DefaultFacetKey)]
    public class MovieDetailCalculatedFacet : Facet
    {
        public const string DefaultFacetKey = "MovieDetailCalculatedFacet";

        public Dictionary<int, MovieVisitDetail> MovieVisits { get; set; }

        public bool AnyXVisitInYDays { get; set; }

        public MovieDetailCalculatedFacet()
        {
            MovieVisits = new Dictionary<int, MovieVisitDetail>();
        }
    }

    [Serializable]
    public class MovieVisitDetail
    {
        public int VisitCount { get; set; }

        public DateTime FirstVisitOnUtc { get; set; }

        public DateTime LastVisitOnUtc { get; set; }
    }
}

I want to increase VisitCount when a contact visits a movie and want to update last visit date. To do this, I created a handler which Sitecore provides.

The handler implements Sitecore's MergingCalculatedFacetHandler<T> class that comes with two methods.
  • UpdateFacet: It takes the current facet and the last interaction parameters. Based on events in last interaction, I check and update facet values. This gets triggered when a session ends.
  • Merge: It takes source and target facets as parameters. This gets triggered when a contact merge operation triggers then this method calls and gives option to combine their calculated facets. You can test this identifying an anonymous contact.
Here is the implementation of UpdateFacet method, if a movie is already in dictionary, visit count is increased, if not, it is added with visit count 1. 

protected override bool UpdateFacet(MovieDetailCalculatedFacet currentFacet, Interaction interaction)
        {
            var isEdited = false;
            var movieDetailVisits = interaction.Events.OfType<MovieDetailEvent>().Where(x => x.DefinitionId == MovieDetailEvent.EventId).ToList();

            if (!movieDetailVisits.Any()) return isEdited;

            foreach (var movieDetailVisit in movieDetailVisits)
            {
                if (currentFacet.MovieVisits.ContainsKey(movieDetailVisit.MovieId))
                {
                    var movieVisit = currentFacet.MovieVisits[movieDetailVisit.MovieId];

                    movieVisit.VisitCount++;
                    movieVisit.LastVisitOnUtc = DateTime.UtcNow;

                    var visits = currentFacet.MovieVisits.Values.ToList();

                    currentFacet.AnyXVisitInYDays = AnyXVisitInYDays(visits);

                    isEdited = true;
                }
                else
                {
                    currentFacet.MovieVisits.Add(movieDetailVisit.MovieId, new MovieVisitDetail
                    {
                        VisitCount = 1,
                        FirstVisitOnUtc = DateTime.UtcNow,
                        LastVisitOnUtc = DateTime.UtcNow
                    });

                    var visits = currentFacet.MovieVisits.Values.ToList();

                    currentFacet.AnyXVisitInYDays = AnyXVisitInYDays(visits);

                    isEdited = true;
                }
            }

            return isEdited;
        }

This method is being called when the session ends.

To make my handler working, I need to register it. So, I registered it to sc.XConnect.Collection.Model.Plugin.xml file which under XConnect instance. The config file will look like this:

  <ICalculatedFacetHandler.MovieDetailVisitHandler>
 <Type>Playground.XConnect.CalculatedFacetHandlers.MovieDetailVisitHandler, Playground.XConnect</Type>
 <As>Sitecore.XConnect.Service.ICalculatedFacetHandler, Sitecore.XConnect.Service</As>
 <LifeTime>Singleton</LifeTime>
 <Options>
  <VisitCount>3</VisitCount>
  <InDays>3</InDays>
  <TrackInDays>30</TrackInDays>
 </Options>
  </ICalculatedFacetHandler.MovieDetailVisitHandler>

<IContactMergeHandler.MovieDetailVisitHandler>
 <Type>Playground.XConnect.CalculatedFacetHandlers.MovieDetailVisitHandler, Playground.XConnect</Type>
 <As>Sitecore.XConnect.Service.IContactMergeHandler, Sitecore.XConnect.Service</As>
 <LifeTime>Singleton</LifeTime>
 <Options>
  <VisitCount>3</VisitCount>
  <InDays>3</InDays>
  <TrackInDays>30</TrackInDays>
 </Options>
</IContactMergeHandler.MovieDetailVisitHandler>

As you see above, you can define parameters too. Then, you can use them in handler class as below:

    public class MovieDetailVisitHandler : MergingCalculatedFacetHandler<MovieDetailCalculatedFacet>
    {
        public MovieDetailVisitHandler(IConfiguration options): this(options.GetValue<int>(nameof(VisitCount)), options.GetValue<int>(nameof(InDays)), options.GetValue<int>(nameof(TrackInDays)))
        {
            
        }
        public MovieDetailVisitHandler(int visitCount, int inDays, int trackInDays) : base(MovieDetailCalculatedFacet.DefaultFacetKey, new[] { new InteractionFacetDependency(WebVisit.DefaultFacetKey, false) })
        {
            VisitCount = visitCount;
            InDays = inDays;
            TrackInDays = trackInDays;
        }

        public int VisitCount { get; set; }

        public int InDays { get; set; }

        public int TrackInDays { get; set; }

How can we test and correct this? 

As I mention before, your handler should be called when a session ends and an interaction gets created. After that, you can check latest ContactFacets created. Your calculated facet should be created as below:

{
 "@odata.type": "#Playground.XConnect.Facets.MovieDetailCalculatedFacet",
 "MovieVisits": [{
   "Key": 3,
   "Value": {
    "VisitCount": 2,
    "FirstVisitOnUtc": "2019-02-07T17:05:53.7771270Z",
    "LastVisitOnUtc": "2019-02-07T17:05:53.7771270Z"
   }
  }, {
   "Key": 5,
   "Value": {
    "VisitCount": 3,
    "FirstVisitOnUtc": "2019-02-07T17:05:53.7771270Z",
    "LastVisitOnUtc": "2019-02-07T17:05:53.7771270Z"
   }
  }
 ],
 "AnyXVisitInYDays": true
}

It is good that you saw your calculated facet has been created. But let's say your calculation is not as expected. No problem, you can easily debug your code.

You just need to attach w3wp.exe process of your xconnect IIS app pool. That's it! Once your session ends, normally your handler should get triggered. If not, you can check if there is any error or if there is any missing thing in your configuration.

I checked and confirmed my calculation. All worked fine. Now, it is time to display in experience profile.

Luckily, we have a great nuget package called EPExpressTab. It provides to add data to experience profile easily.  I've just downloaded the package and implemented the view model as below:

namespace Playground.Models
{
    public class MovieVisitView : EpExpressViewModel
    {
        public override string Heading => "Movie Visit History";
        public override string TabLabel => "Movie Visits";
        public override object GetModel(Guid contactId)
        {
            var contact = EPRepository.GetContact(contactId, MovieDetailCalculatedFacet.DefaultFacetKey);

            var MovieDetailFacet = contact.GetFacet<MovieDetailCalculatedFacet>();

            var viewModel = new MovieVisitViewModel();

            if (MovieDetailFacet == null) return viewModel;

            viewModel.MovieVisits = MovieDetailFacet.MovieVisits.Select(x => new MovieVisit()
            {
                MovieId = x.Key, VisitDetail = new VisitDetail
                {
                    FirstVisitOnUtc = x.Value.FirstVisitOnUtc,
                    LastVisitOnUtc = x.Value.LastVisitOnUtc,
                    VisitCount = x.Value.VisitCount
                }
            }).ToList();

            return viewModel;
        }

        public override string GetFullViewPath(object model)
        {
            return "/Views/Movies/ExperienceProfileTab.cshtml";
        }
    }
}

I've put a html table to ExperienceProfileTab.cshtml file and binded my data. Here is the result.

I will continue with creating segmentation rules and this calculated facet will help me on that.

Comments

Popular posts from this blog

Sitecore 9 - Custom Page Events & Filtering with XConnect

This is the first article of a series. I am going to start with creating a custom page event and will show how we can fetch event data using xconnect api. Let's start with reminding demo scenario: Imagine that you have a website displaying movies. Visitors are able to see movie details and take some actions like save movie or share it.  You want to follow the visitors' activities and you want to take some marketing actions based on those activities. For example, if a contact visits a movie more than X time or she/he saves a movie, you want to send those movies to an external system. In addition, there is going to be a limit to send same movie. Such as, it will not be possible to send same movie more than 2 times.  You want to configure this as a marketing automation plan to give flexibility to your marketing managers. They should be able to add configurable rules and activities.  My first focus is movie detail page. I want to track visitors when they visit the

Sitecore Commerce – XC9 Tips – Missing Commerce Components in SXA Toolbox on Experience Editor

I've recently had an issue that commerce components were missing in SXA Toolbox. I setup Sitecore Commerce on top of an existing instance and I already had a SXA website working on it. The idea was to add commerce components and functionality to my existing website. But after commerce setup, the toolbox was still showing default SXA components and commerce components were missing although I add commerce tenant and website modules: I checked Available Renderings under Presentation folder, there was no problem, commerce renderings were there. I created another tenant and website to see if it shows the commerce components in toolbox. Nothing seemed different but I was seeing commerce components for new website and it was missing on existing one. Then, I noticed two things: 1- Selected catalog was empty in content editor (/sitecore/Commerce/Catalog Management/Catalogs) even if I see Habitat_Master catalog in Merchandising section on commerce management panel. 2- Bootstrap

Sitecore Commerce – XC9 Tips – Configuring Postman

In this post, I am going to show how to setup postman and run the scripts. When you download Sitecore Commerce, it includes ready to use postman sample requests (inside Sitecore.Commerce.Engine.SDK.x.x.x folder). You need to import that postman folder to postman app. The steps are written in official documentation , just click import and choose postman folder. You will see some parameters in sample requests such as  {{SitecoreIdServerHost}}, {{OpsApiHost}}, {{OpsApi}} Those are environment based variables that you can change according to your environment details. When you click "setting" icon, you will see predefined environments: AdventureWorks and Habitat. For example, click "Habitat Environment" to edit its variables. You see that AuthoingHost is using port 5000, it is the default port if the that port is available. You can check which port is using your Authoring role from IIS. If your Authoring port is different than 5000, you ne