Intro and Prerequisites
Like other Sitecore enthusiasts, I have been extremely impressed with the new technology and architecture behind xDBAs Nick Wesselman puts it: "The Experience Database provides data structures and APIs to allow you to track an individual across devices, provided you can identify him/her by some unique identifier such as an email address. This is key, otherwise you just end up having a ton of anonymous contacts in your database."
If you haven't read his blog, I highly recommend it. It got me on the right path with all things xDB / Mongo: http://www.techphoria414.com/Blog
Reading through his "One Month with Sitecore 7.5, Part 5: Persisting Contact Data to xDB" post where he talks about Contact Facets will help you better understand what I am doing below.
Also make sure you check out Martina Welander's posts as well: http://sitecore-community.github.io/docs/documentation/xDB/Facets/. She has tons of good stuff in there.
The Goal
As you can see, the code for identifying a user is straightforward:1: var tracker = Sitecore.Analytics.Tracker.Current;
2: if (tracker == null || !tracker.IsActive || tracker.Contact == null)
3: {
4: return;
5: }
6: tracker.Session.Identify("some unique identifier");
This is nice and all, but it doesn't help with populating my Experience Profile (xFile).
Wouldn't it be awesome if I could identify the user, and then check my CRM system to see if it contained profile data about the user, and if so, populate the xFile using that information?
Wouldn't it be even more awesome if after I identified the user, I could make sure that my xFile always kept in sync with my CRM system?
Having extensive Dynamics CRM development experience, I decided to write up a POC to make this happen.
NOTE: What I am doing here is a one-way sync from Dynamics CRM to Sitecore. We could very easily take this one step further with a more advanced contact form, and update data in CRM from Sitecore.
Identify User and Flush-To-Mongo Control
My first task was to put together a user control that would allow me to identify the user and then flush the session data to Mongo so that I could review the outcome.
The basic control consisted of 1 text box and 2 buttons. The text box would be used to capture the user's email address. Clicking the first button would identify the user using the code shown above, while clicking the second button would flush the session data to Mongo.
Flushing the session data can be achieved by simply making a call to end the session: Session.Abandon()
Here is the code-behind for my control:
Flushing the session data can be achieved by simply making a call to end the session: Session.Abandon()
Here is the code-behind for my control:
1: public partial class IdentifyUser : System.Web.UI.UserControl
2: {
3: private void Page_Load(object sender, EventArgs e)
4: {
5: btnIdentifyUser.Click += btnIdentifyUser_Click;
6: btnFlushToMongo.Click += btnFlushToMongo_Click;
7: lblIdentiedUser.Text = Tracker.Current.Contact.Identifiers.Identifier;
8: }
9: private void btnIdentifyUser_Click(Object sender, EventArgs e)
10: {
11: Tracker.Current.Session.Identify(txtCurrentUser.Text);
12: string myScript = "alert('Done!');";
13: Page.ClientScript.RegisterStartupScript(GetType(), new Guid().ToString(), myScript, true);
14: }
15: private void btnFlushToMongo_Click(Object sender, EventArgs e)
16: {
17: Session.Abandon();
18: string myScript = "alert('Flushed!');";
19: Page.ClientScript.RegisterStartupScript(GetType(), new Guid().ToString(), myScript, true);
20: }
21: }
Nothing elegant about this at all (especially because I used WebForms over MVC). I simply slapped this on the out-of-the-box Sitecore demo page and the result looked like this:
You will notice that I also had a label on line 7 where I would populate the contact's identifier so that I could make sure that the tracker is working as expected.
Working with Dynamics CRM
Creating a Proxy Class
When coding for Dynamics CRM, you need to use the "CrmSvcUtil" command-line code generation tool, available in the CRM SDK, in order to generate early-bound classes that represent the entity data model in your CRM system.So in short; this tool located in the bin folder of the SDK is used to create a proxy class from the CRM Web Service of your instance.
For more information about this, you can check out this link from Microsoft: https://msdn.microsoft.com/en-us/library/gg327844.aspx
I named my generated class file "CrmModels.cs" and threw it into my project.
The xFileManager
I started putting together a manager class that I would use to facilitate finding a user / contact match in Dynamics CRM and then populate the xFile using the contact record's data.I added a Dynamics "contact" entity property to my class called CrmContact. The plan was to populate this if I had a match in CRM based on email address, and then use it to hydrate my various Sitecore xFile contact facet objects.
Finding the contact in Dynamics CRM
The first method I added to my class was to make a call out to Dynamics to see if I had a contact match based on the identified user's email address. If I did find the user, it would set my CrmContact property to the match and return true.1: private bool FoundCrmContact(string emailAddress)
2: {
3: var connection = new CrmConnection("MTCCRMConnString");
4: var org = new OrganizationService(connection);
5: var crmDataContext = new CRMDataContext(org);
6: var contactMatch = crmDataContext.ContactSet.Where(t => t.EMailAddress1.Equals(emailAddress));
7: if (contactMatch.FirstOrDefault() != null)
8: {
9: CrmContact = contactMatch.FirstOrDefault();
10: return true;
11: }
12: return false;
13: }
Syncing CRM contact data with the identified contact's facets
Next, I focused on writing a method that would populate the contact's various facets with the CRM data if a match was found.
Here is my method:
1: public void SyncCrmXFile(Sitecore.Analytics.Tracking.Contact identifiedUser)
2: {
3: if (identifiedUser.Identifiers.IdentificationLevel == ContactIdentificationLevel.Known)
4: {
5: //Get contact unique indentifier, in our case the email address
6: var contactEmail = identifiedUser.Identifiers.Identifier;
7: if (FoundCrmContact(contactEmail))
8: {
9: #region Get Facets
10: var emailFacet = identifiedUser.GetFacet<IContactEmailAddresses>("Emails");
11: var addressFacet = identifiedUser.GetFacet<IContactAddresses>("Addresses");
12: var personalFacet = identifiedUser.GetFacet<IContactPersonalInfo>("Personal");
13: var phoneFacet = identifiedUser.GetFacet<IContactPhoneNumbers>("Phone Numbers");
14: IEmailAddress email = emailFacet.Entries.Contains("Work Email")
15: ? emailFacet.Entries["Work Email"]
16: : emailFacet.Entries.Create("Work Email");
17: IAddress address = addressFacet.Entries.Contains("Work Address")
18: ? addressFacet.Entries["Work Address"]
19: : addressFacet.Entries.Create("Work Address");
20: IPhoneNumber workPhone = phoneFacet.Entries.Contains("Work Phone")
21: ? phoneFacet.Entries["Work Phone"]
22: : phoneFacet.Entries.Create("Work Phone");
23: #endregion
24: #region Update Facets with CRM Contact Data
25: email.SmtpAddress = CrmContact.EMailAddress1;
26: emailFacet.Preferred = "Work Email";
27: address.StreetLine1 = CrmContact.Address1_Line1;
28: address.StreetLine2 = CrmContact.Address1_Line2;
29: address.PostalCode = CrmContact.Address1_PostalCode;
30: personalFacet.Title = CrmContact.Salutation;
31: personalFacet.FirstName = CrmContact.FirstName;
32: personalFacet.MiddleName = CrmContact.MiddleName;
33: personalFacet.Surname = CrmContact.LastName;
34: //personalFacet.Gender = CrmContact.GenderCode.Value;
35: personalFacet.JobTitle = CrmContact.JobTitle;
36: personalFacet.BirthDate = CrmContact.BirthDate;
37: workPhone.CountryCode = workPhone.Number = CrmContact.Telephone1;
38: #endregion
39: Sitecore.Diagnostics.Log.Info(string.Format("Successfully synced Dynamics CRM known user: {0} {1}", CrmContact.FirstName, CrmContact.LastName), this);
40: }
41: }
42: }
In summary, the code above does the following:
- Accepts a Sitecore.Analytics.Tracking.Contact object as a parameter
- Makes sure that we had already identified the contact
- Pulls out the identified contact's email address and looks for a match in CRM
- If a match exists in CRM, it uses the data to populate / sync the identified contact's facets
1: Tracker.Current.Session.Identify(txtCurrentUser.Text);
2: var xFile = new XFileManager();
3: xFile.SyncCrmXFile(Tracker.Current.Contact);
Identify, Flush and Review
With all these pieces in place, I was ready to do some testing. In my example below, I am using Stefan Edberg as my identified contact.Side note
If you don't know who Stefan Edberg is, he was an awesome tennis player; one of my idols when growing up. He is currently the tennis coach of Roger Federer.
Here he is as a contact in our Dynamics CRM system:
So, I went ahead and identified Stefan using his email address. Things were starting to look good:
Next, I wanted to flush the session data to Mongo, and review what was populated in our Experience Profile dashboard:
Nice! There he is. Clicking on his name and then the details tab revealed the rest of the data that was populated using the CRM contact data:
Keeping things in sync
What I wanted to do next was make sure that my xFile always contained the most up-to-date data from my CRM system.The key was finding the sweet spot in the "Session End" pipeline before the actual contact data gets flushed to Mongo.
Looking through some of the analytics config files, and digging a little bit around the analytics assemblies, I found my spot before the Sitecore.Analytics.Pipelines.SessionEnd.InitializeTracker processor.
So, my processor was simply:
1: public class SyncCrm
2: {
3: public void Process(PipelineArgs args)
4: {
5: Assert.ArgumentNotNull(args, "args");
6: if (!AnalyticsSettings.Enabled)
7: {
8: return;
9: }
10: RunCrmSync();
11: }
12: private void RunCrmSync()
13: {
14: var xFile = new XFileManager();
15: xFile.SyncCrmXFile(Tracker.Current.Session.Contact);
16: }
17: }
My config file looked like this:
1: <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
2: <sitecore>
3: <pipelines>
4: <sessionEnd>
5: <processor type="Arke.Sandbox.Xrm.Pipelines.Analytics.SyncCrm, Arke.Sandbox.Xrm" patch:before="processor[@type='Sitecore.Analytics.Pipelines.SessionEnd.InitializeTracker, Sitecore.Analytics']" />
6: </sessionEnd>
7: </pipelines>
8: </sitecore>
9: </configuration>