Extending Sitecore Experience Profile in Sitecore 9

In this post we will look at how to display the custom contact facets in the Sitecore Experience Profile. We looked at the posts by Jonathan Robbins AKA ISlayTitans (Obviously he likes slaying Titans) and we needed to customize it for Sitecore 9.

First, we added two new Interaction facets as shown below:

    1. using System;
    2. using Sitecore.XConnect;
    3.  
    4. namespace Konabos.XConnect.Loyalty.Model.Facets
    5. {
    6. [FacetKey(DefaultFacetKey)]
    7. [Serializable]
    8. public class LoyaltyOrderInfoFacet : Facet //Interaction Facet: Information about the order placed during the interaction
    9. {
    10. public const string DefaultFacetKey = "LoyaltyOrderInfoFacet";
    11. public string OrderId { get; set; }
    12. public double OrderTotal { get; set; }
    13. public int PointsEarned { get; set; }
    14. public int PointsSpent { get; set; }
    15. }
    16. }
    1. using System;
    2. using Sitecore.XConnect;
    3.  
    4. namespace Konabos.XConnect.Loyalty.Model.Facets
    5. {
    6. [FacetKey(DefaultFacetKey)]
    7. [Serializable]
    8. public class LoyaltyInteractionFacet : Facet //Interaction Facet: Interactions such as registered, bought 2x of a product, performed an action
    9. {
    10. public const string DefaultFacetKey = "LoyaltyInteractionFacet";
    11. public string ActionTaken { get; set; }
    12. public int PointsEarned { get; set; }
    13. public int PointsSpent { get; set; }
    14. }
    15. }
    1. using Konabos.XConnect.Loyalty.Model.Events;
    2. using Konabos.XConnect.Loyalty.Model.Facets;
    3. using Sitecore.XConnect;
    4. using Sitecore.XConnect.Schema;
    5.  
    6. namespace Konabos.XConnect.Loyalty.Model.Models
    7. {
    8. public class LoyaltyModel
    9. {
    10. public static XdbModel Model { get; } = BuildModel();
    11.  
    12. private static XdbModel BuildModel()
    13. {
    14. XdbModelBuilder modelBuilder = new XdbModelBuilder("LoyaltyModel", new XdbModelVersion(1, 3));
    15.  
    16. modelBuilder.DefineFacet<Contact, LoyaltyPointsFacet>(FacetKeys.LoyaltyPointsFacet);
    17. modelBuilder.DefineFacet<Interaction, LoyaltyOrderInfoFacet>(FacetKeys.LoyaltyOrderInfoFacet);
    18. modelBuilder.DefineFacet<Contact, LoyaltyCommerceFacet>(FacetKeys.LoyaltyCommerceFacet);
    19. modelBuilder.DefineFacet<Interaction, LoyaltyInteractionFacet>(FacetKeys.LoyaltyInteractionFacet);
    20.  
    21. modelBuilder.DefineEventType<LoyaltyCommerceEvent>(false);
    22.  
    23. modelBuilder.ReferenceModel(Sitecore.XConnect.Collection.Model.CollectionModel.Model);
    24.  
    25. return modelBuilder.BuildModel();
    26. }
    27. }
    28. public class FacetKeys
    29. {
    30. public const string LoyaltyOrderInfoFacet = "LoyaltyOrderInfoFacet";
    31. public const string LoyaltyPointsFacet = "LoyaltyPointsFacet";
    32. public const string LoyaltyCommerceFacet = "LoyaltyCommerceFacet";
    33. public const string LoyaltyInteractionFacet = "LoyaltyInteractionFacet";
    34. }
    35. }

Once that model was created, we pushed it to the xConnect and the Sitecore websites.

Next we tackle the website. Lets look at the configs and jump into code:

    1. <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    2. <sitecore>
    3. <pipelines>
    4. <group groupName="ExperienceProfileContactViews">
    5. <pipelines>
    6. <loyaltyactions>
    7. <processor type="Feature.Konabos.Loyalty.Website.Pipelines.ContactFacets.Loyalty.ConstructLoyaltyDataTable, Feature.Konabos.Loyalty.Website" />
    8. <processor type="Feature.Konabos.Loyalty.Website.Pipelines.ContactFacets.Loyalty.GetLoyaltyActions, Feature.Konabos.Loyalty.Website"/>
    9. <processor type="Sitecore.Cintel.Reporting.Processors.ApplySorting, Sitecore.Cintel"/>
    10. <processor type="Sitecore.Cintel.Reporting.Processors.ApplyPaging, Sitecore.Cintel"/>
    11. </loyaltyactions>
    12. </pipelines>
    13. </group>
    14. </pipelines>
    15. </sitecore>
    16. </configuration>
    1. <?xml version="1.0"?>
    2. <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    3. <sitecore>
    4. <experienceProfile>
    5. <resultTransformManager>
    6. <resultTransformProvider>
    7. <intelResultTransformers>
    8. <add viewName="loyaltyactions" type="Feature.Konabos.Loyalty.Website.Pipelines.ContactFacets.Loyalty.LoyaltyActionsTransformer, Feature.Konabos.Loyalty.Website"/>
    9. </intelResultTransformers>
    10. </resultTransformProvider>
    11. </resultTransformManager>
    12. </experienceProfile>
    13. </sitecore>
    14. </configuration>

The ExperienceProfileContactViews pipeline needs to be customized. First lets look at ConstructLoyaltyDataTable which will create a DataTable and the columns which will hold the data we want to use.

    1. using System.Data;
    2. using Sitecore.Cintel.Reporting;
    3. using Sitecore.Cintel.Reporting.Processors;
    4.  
    5. namespace Feature.Konabos.Loyalty.Website.Pipelines.ContactFacets.Loyalty
    6. {
    7. public class ConstructLoyaltyDataTable : ReportProcessorBase
    8. {
    9. public override void Process(ReportProcessorArgs args)
    10. {
    11. args.ResultTableForView = new DataTable();
    12. args.ResultTableForView.Columns.Add(new ViewField<string>("LoyaltyAction").ToColumn());
    13. args.ResultTableForView.Columns.Add(new ViewField<int>("PointsEarned").ToColumn());
    14. args.ResultTableForView.Columns.Add(new ViewField<int>("PointsSpent").ToColumn());
    15. }
    16. }
    17. }

Next we need to populate the DataTable, in this case, I am calling xConnect to get some information.

    1. using System;
    2. using Sitecore.Cintel.Reporting;
    3. using Sitecore.Cintel.Reporting.Processors;
    4. using Sitecore.XConnect.Client;
    5. using Sitecore.XConnect.Client.Configuration;
    6. using Sitecore.XConnect;
    7. using Konabos.XConnect.Loyalty.Model.Facets;
    8. using System.Linq;
    9. namespace Feature.Konabos.Loyalty.Website.Pipelines.ContactFacets.Loyalty
    10. {
    11. public class GetLoyaltyActions : ReportProcessorBase
    12. {
    13. public override void Process(ReportProcessorArgs args)
    14. {
    15. Guid contactId = args.ReportParameters.ContactId;
    16. var resultTableForView = args.ResultTableForView;
    17.  
    18. using (XConnectClient client = SitecoreXConnectClientConfiguration.GetClient("xconnect/clientconfig"))
    19. {
    20. ContactExpandOptions contactExpandOptions = new ContactExpandOptions()
    21. {
    22. Interactions = new RelatedInteractionsExpandOptions(LoyaltyInteractionFacet.DefaultFacetKey)
    23. };
    24. ContactReference contactReference = new ContactReference(contactId);
    25. var contact = client.Get<Contact>((IEntityReference<Contact>)contactReference, (ExpandOptions)contactExpandOptions);
    26. var interactions = contact.Interactions.Where(i => i.Facets.FirstOrDefault().Key == LoyaltyInteractionFacet.DefaultFacetKey);
    27.  
    28. foreach (var interaction in interactions)
    29. {
    30. var dataRow = resultTableForView.NewRow();
    31. var facet = interaction.GetFacet<LoyaltyInteractionFacet>();
    32. dataRow["LoyaltyAction"] = facet.ActionTaken;
    33. dataRow["PointsEarned"] = facet.PointsEarned;
    34. dataRow["PointsSpent"] = facet.PointsSpent;
    35. resultTableForView.Rows.Add(dataRow);
    36. }
    37. args.QueryResult = resultTableForView;
    38. }
    39. }
    40. }
    41. }

We need to allow the Sitecore admin screen to sort and page through the data shown. We add the ApplySorting and ApplyPaging processors at the end to complete the configuration.

We need to finish up the code by adding code that transforms the DataTable and also adds any additional properties to the results.

    1. using System.Data;
    2. using Sitecore.Cintel.Client;
    3. using Sitecore.Cintel.Client.Transformers;
    4. using Sitecore.Cintel.Commons;
    5. using Sitecore.Cintel.Endpoint.Transfomers;
    6. using Sitecore.Diagnostics;
    7.  
    8. namespace Feature.Konabos.Loyalty.Website.Pipelines.ContactFacets.Loyalty
    9. {
    10. public class LoyaltyActionsTransformer : IIntelResultTransformer, IResultTransformer<DataTable>
    11. {
    12. private ResultSetExtender resultSetExtender;
    13.  
    14. public LoyaltyActionsTransformer()
    15. {
    16. this.resultSetExtender = ClientFactory.Instance.GetResultSetExtender();
    17. }
    18.  
    19. public LoyaltyActionsTransformer(ResultSetExtender resultSetExtender)
    20. {
    21. this.resultSetExtender = resultSetExtender;
    22. }
    23.  
    24. public object Transform(ResultSet<DataTable> resultSet)
    25. {
    26. Assert.ArgumentNotNull((object)resultSet, "resultSet");
    27. this.resultSetExtender.AddRecency(resultSet, "VisitStartDateTime");
    28. return (object)resultSet;
    29. }
    30. }
    31. }

The next part was super painful and Kamruz Jaman spent a ton of time on and is based on https://jonathanrobbins.co.uk/2016/04/19/extending-sitecore-experience-profile-speak-app/. I am going to skip over this part and show you some screenshots.

Once those are setup, you need to get the JavaScript file ready. Here is an example of the file we used and make sure the name used matches the processor grouping we had in the configs. Configure it accordingly using Sitecore Rocks on the Panel item.

    1. define(["sitecore", "/-/speak/v1/experienceprofile/DataProviderHelper.js"], function (sc, providerHelper) {
    2. var app = sc.Definitions.App.extend({
    3. initialized: function () {
    4. var tableName = "loyaltyactions"; // Case Sensitive!
    5. var localUrl = "/intel/" + tableName;
    6.  
    7. providerHelper.setupHeaders([
    8. { urlKey: localUrl + "?", headerValue: tableName }
    9. ]);
    10.  
    11. var url = sc.Contact.baseUrl + localUrl;
    12.  
    13. providerHelper.initProvider(this.LoyaltyActionsDataProvider, tableName, url, this.LoyaltyActionsTabMessageBar);
    14. providerHelper.subscribeSorting(this.LoyaltyActionsDataProvider, this.LoyaltyActions);
    15. providerHelper.getListData(this.LoyaltyActionsDataProvider);
    16.  
    17. providerHelper.subscribeAccordionHeader(this.LoyaltyActionsDataProvider, this.LoyaltyActionsAccordion);
    18.  
    19. sc.Contact.subscribeVisitDialog(this.LoyaltyActions);
    20. }
    21. });
    22. return app;
    23. });

It does take quite a bit to do this for the first one but it becomes easier to add more customizations. Here is the end result.

Last but not the least, how do we update the photo of the contact? Here is the code:

    1. Contact existingContact = client.Get<Contact>(reference, new ContactExpandOptions(new string[] { PersonalInformation.DefaultFacetKey, LoyaltyPointsFacet.DefaultFacetKey, LoyaltyCommerceFacet.DefaultFacetKey, Avatar.DefaultFacetKey }));
    2.  
    3. Avatar avatar = existingContact.GetFacet<Avatar>(Avatar.DefaultFacetKey);
    4. if (avatar == null)
    5. {
    6. var imageUrl = "https://www.konabos.com/wp-content/uploads/team/akshay-150x150.jpg";
    7. var webClient = new WebClient();
    8. // Download data from URL
    9. byte[] imageBytes = webClient.DownloadData(imageUrl);
    10. if (imageBytes != null)
    11. {
    12. avatar = new Avatar("image/jpeg", imageBytes);
    13. }
    14. client.SetFacet<Avatar>(existingContact, Avatar.DefaultFacetKey, avatar);
    15. client.Submit();
    16. }

If you have any questions or concerns, please get in touch with me. (@akshaysura13 on twitter or on Slack).

Reference: https://jonathanrobbins.co.uk/2016/03/15/extending-sitecore-experience-profile-experienceprofilecontactviews/