Sitecore Contacts – Create and save contacts to and from xDB (MongoDB)

The Sitecore Contact is the cornerstone of the Sitecore Experience Platform and is the place where you store every data you know about any contact, named and anonymous.

UPDATE: 09-09-2016: CreateContact Updated: Thanks to moginheden for the update.

UPDATE: 27-09-2016: Repository Updated. In certain situations it would be possible for the class to go into an infinite loop. Read more here: Sitecore General error when submitting contact – Another contact with the same identifier already exists. Thanks to Dan and others for the update. 

This library was made by my colleague Peter Wind for a project we are both working on. In some cases we need to manipulate a contact that is not currently identified, for example when updating contact facets from imported data.
To do so you need to find the contact in xDB. If it does not exist, you need to create the contact. And when you are done updating the contact, you must save the data back to xDB and release the lock on the contact.

Any Contact in Sitecore is identified by a string. There is no connection between the user database (the .NET Security Provider) and a contact other than the one you make yourself. The username IS your key, and the key should be unique. You must be careful when identifying a Sitecore User, and never identify extranet\anonymous.

A WORD OF CAUTION:

The code uses some direct calls to the Sitecore Analytics and thus explores some undocumented features that was not meant to be called directly. The code is therefore a result of trial-and-error plus help from Sitecore Support. In other words: Just because it works on my 500.000+ contacts, it might fail on yours.

ENOUGH TALK LETS CODE:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using Sitecore.Analytics;
using Sitecore.Analytics.Data;
using Sitecore.Analytics.DataAccess;
using Sitecore.Analytics.Model;
using Sitecore.Analytics.Tracking;
using Sitecore.Analytics.Tracking.SharedSessionState;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;

namespace MyNamespace
{
  public class ExtendedContactRepository
  {
    private const int _MAX_RETRIES = 10;
    private int _retries;

    public Contact GetOrCreateContact(string userName)
    {
      if (IsContactInSession(userName))
        return Tracker.Current.Session.Contact;

      ContactRepository contactRepository = Factory.CreateObject("tracking/contactRepository", true) as ContactRepository;
      ContactManager contactManager = Factory.CreateObject("tracking/contactManager", true) as ContactManager;

      Assert.IsNotNull(contactRepository, "contactRepository");
      Assert.IsNotNull(contactManager, "contactManager");

      try
      {
        Contact contact = contactRepository.LoadContactReadOnly(userName);
        LockAttemptResult<Contact> lockAttempt;

        if (contact == null)
          lockAttempt = new LockAttemptResult<Contact>(LockAttemptStatus.NotFound, null, null);
        else
          lockAttempt = contactManager.TryLoadContact(contact.ContactId);

        return GetOrCreateContact(userName, lockAttempt, contactRepository, contactManager);
      }
      catch (Exception ex)
      {
        throw new Exception(this.GetType() + " Contact could not be loaded/created - " + userName, ex);
      }
    }

    public void ReleaseAndSaveContact(Contact contact)
    {
      ContactManager manager = Factory.CreateObject("tracking/contactManager", true) as ContactManager;
      if (manager == null)
        throw new Exception(this.GetType() +  " Could not instantiate " + typeof(ContactManager));
      manager.SaveAndReleaseContact(contact);
      ClearSharedSessionLocks(manager, contact);
    }
    
    private Contact GetOrCreateContact(string userName, LockAttemptResult<Contact> lockAttempt, ContactRepository contactRepository, ContactManager contactManager)
    {
      switch (lockAttempt.Status)
      {
        case LockAttemptStatus.Success:
          Contact lockedContact = lockAttempt.Object;
          lockedContact.ContactSaveMode = ContactSaveMode.AlwaysSave;
          return lockedContact;

        case LockAttemptStatus.NotFound:
          Contact createdContact = CreateContact(userName, contactRepository);
          contactManager.FlushContactToXdb(createdContact);
          // Avoid going into an infinite loop.
          _retries++;
          if (_retries >= _MAX_RETRIES)
            throw new Exception(string.Format("ExtendedContactRepository: Contact {0} could not be created. ", username));
          return GetOrCreateContact(userName);

        default:
          throw new Exception(this.GetType() + " Contact could not be locked - " + userName);
      }
    }

    private Contact CreateContact(string userName, ContactRepository contactRepository)
    {
      Contact contact = contactRepository.CreateContact(ID.NewID);
      contact.Identifiers.Identifier = userName;
      contact.Identifiers.IdentificationLevel = Sitecore.Analytics.Model.ContactIdentificationLevel.Known;
      contact.System.Value = 0;
      contact.System.VisitCount = 0;
      contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
      return contact;
    }

    private bool IsContactInSession(string userName)
    {
      var tracker = Tracker.Current;

      if (tracker != null && 
	      tracker.IsActive && 
		  tracker.Session != null && 
		  tracker.Session.Contact != null && 
		  tracker.Session.Contact.Identifiers != null && 
		  tracker.Session.Contact.Identifiers.Identifier != null && 
		  tracker.Session.Contact.Identifiers.Identifier.Equals(userName, StringComparison.InvariantCultureIgnoreCase))
        return true;

      return false;
    }

    private void ClearSharedSessionLocks(ContactManager manager, Contact contact)
    {
      if (HttpContext.Current != null && HttpContext.Current.Session != null)
        return;

      var sharedSessionStateManagerField = manager.GetType().GetField("sharedSessionStateManager", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
      Assert.IsNotNull(sharedSessionStateManagerField, "Didn't find field 'sharedSessionStateManager' in type '{0}'.", typeof(ContactManager));
      var sssm = (SharedSessionStateManager)sharedSessionStateManagerField.GetValue(manager);
      Assert.IsNotNull(sssm, "Shared session state manager field value is null.");

      var contactLockIdsProperty = sssm.GetType().GetProperty("ContactLockIds", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
      Assert.IsNotNull(contactLockIdsProperty, "Didn't find property 'ContactLockIds' in type '{0}'.", sssm.GetType());
      var contactLockIds = (Dictionary<Guid, object>)contactLockIdsProperty.GetValue(sssm);
      Assert.IsNotNull(contactLockIds, "Contact lock IDs property value is null.");
      contactLockIds.Remove(contact.ContactId);
    }
  }
}

HOW TO USE THE CODE:

// Create an instance of the repository
ExtendedContactRepository extendedContactRepository = new ExtendedContactRepository();
// Get a contact by a username
Contact contact = extendedContactRepository.GetOrCreateContact(userName);

// Do some code that updates the contact
// For example update a facet:
// https://briancaos.wordpress.com/2015/07/16/sitecore-contact-facets-create-your-own-facet/

// Save the contact
extendedContactRepository.ReleaseAndSaveContact(contact);

SOME EXPLANATION:

The 2 public methods, GetOrCreateContact() and ReleaseAndSaveContact(), are the getter and setter methods.

GetOrCreateContact() tries to get a lock on a Contact. If the lock is successful, a Contact is found and the Contact can be returned.  If not, no Contact is found and we create one.

ReleaseAndSaveContact() saves and releases the contact which means that the data is stored in the Shared Session, and the contact is released; the ClearSharedSessionLocks() attempts to release the locks from the Sitecore Shared Session  State Database. Please note that the data is still not stored directly in xDB but in the Shared Session, and data is flushed when the session expires. The trick is that we open the contact in write-mode, and release the Contact after the update, making it available immediately after by other threads.

Generally, when using the Sitecore ContactManager(), data is not manipulated directly. Only when using the Sitecore ContactRepository() you update xDB directly, but Sitecore does not recommend this, as it may have unwanted side effects.

MORE TO READ:

 

About briancaos

Developer at Pentia A/S since 2003. Have developed Web Applications using Sitecore Since Sitecore 4.1.
This entry was posted in .net, c#, General .NET, Sitecore 8 and tagged , , , , , , , , . Bookmark the permalink.

21 Responses to Sitecore Contacts – Create and save contacts to and from xDB (MongoDB)

  1. Pingback: Sitecore List Manager – Add Contacts to EXM Lists | Brian Pedersen's Sitecore and .NET Blog

  2. Pingback: How to Identify and Merge Contacts in Sitecore xDB | Exercising Sitecore

  3. Pingback: Sitecore EXM: Send an email from code | Brian Pedersen's Sitecore and .NET Blog

  4. Dan says:

    This is an old post, but in case someone comes across it: there’s a possible path through GetOrCreateContact(string userName)
    that calls GetOrCreateContact(string userName, LockAttemptResult lockAttempt, ContactRepository contactRepository, ContactManager contactManager)

    That method, in turn, has a path that can call GetOrCreateContact(string userName) , creating an infinite loop and eventually a stack overflow. I’m experimenting with this fix:

    private Sitecore.Analytics.Tracking.Contact GetOrCreateContact(string userName)
    {
    var contactRepository = Factory.CreateObject(“tracking/contactRepository”, true) as ContactRepository;
    if (contactRepository == null)
    {
    throw new NullReferenceException(“Cannot create instance of RepositoryManager”);
    }
    var contactManager = Factory.CreateObject(“tracking/contactManager”, true) as ContactManager;
    if (contactManager == null)
    {
    throw new NullReferenceException(“Cannot create instance of ContactManager”);
    }

    try
    {
    var contact = contactRepository.LoadContactReadOnly(userName);
    LockAttemptResult lockAttempt;

    if (contact == null)
    {
    lockAttempt = new LockAttemptResult(
    LockAttemptStatus.NotFound,
    null,
    null);
    }
    else
    {
    lockAttempt = contactManager.TryLoadContact(contact.ContactId);
    }
    //we pass true for the don’t try again field to prevent a stack overflow error
    return this.GetOrCreateContact(userName, lockAttempt, contactRepository, contactManager, true);
    }
    catch (Exception ex)
    {
    throw new Exception(this.GetType() + ” Contact could not be loaded/created – ” + userName, ex);
    }
    }

    private Sitecore.Analytics.Tracking.Contact GetOrCreateContact(
    string userName,
    LockAttemptResult lockAttempt,
    ContactRepository contactRepository,
    ContactManager contactManager,
    bool DontTryAgain)
    {
    switch (lockAttempt.Status)
    {
    case LockAttemptStatus.Success:
    var lockedContact = lockAttempt.Object;
    lockedContact.ContactSaveMode = ContactSaveMode.AlwaysSave;
    return lockedContact;

    case LockAttemptStatus.NotFound:
    var createdContact = this.CreateContact(userName, contactRepository);
    contactManager.FlushContactToXdb(createdContact);
    //this is to prevent a stack overflow exception
    if (!DontTryAgain)
    {
    return this.GetOrCreateContact(userName);
    }
    else
    {
    throw new Exception(this.GetType() + ” Contact could not be locked – ” + userName);
    }
    default:
    throw new Exception(this.GetType() + ” Contact could not be locked – ” + userName);
    }
    }

    Like

  5. moginheden says:

    There is a problem with the article’s code. When it makes a new contact it sets the identifier, but does not set the IdentificationLevel of it to known. This caused all kinds of funky errors that were a nightmare to track down where users who logged in and had Identify() called were being merged with other users who logged in from the same computer.

    The corrected code is below, (just 1 line added.)

    private Contact CreateContact(string userName, ContactRepository contactRepository)
    {
    Contact contact = contactRepository.CreateContact(ID.NewID);
    contact.Identifiers.IdentificationLevel = Sitecore.Analytics.Model.ContactIdentificationLevel.Known;
    contact.Identifiers.Identifier = userName;
    contact.System.Value = 0;
    contact.System.VisitCount = 0;
    contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
    return contact;
    }

    Like

  6. briancaos says:

    Yes I believe you are right. I have updated the method. Thanks for the feedback :)

    Like

  7. gorhal says:

    Great post as always :-)
    I have a question regarding creation of contacts. Why do you create a contact ? I mean the contact is already created(by Sitecore when you entered the site) but it is anonymous. So instead of creating it why not identify it instead. Or am I missing something here

    Like

  8. briancaos says:

    Contact creation is about named contacts, and you can create a named contact with 2 different methods:

    1) Identify the contact using Tracker.Identify
    2) Create it using this code.

    The first approach requires the user to be logged in, in session, and you need a Sitecore context.
    The last approach requires you to have a contact name and access to Sitecore.

    Therefore the last approach can be used in Pipeline, Scheduled Tasks, in background jobs whereas the first approach can only be used in current session and context.

    Liked by 1 person

  9. gorhal says:

    Yes of course, I totally forgot the pipeline/scheduled task approach. Thanks :-)

    Like

  10. Pingback: Sitecore General error when submitting contact – Another contact with the same identifier already exists | Brian Pedersen's Sitecore and .NET Blog

  11. Bryan Thrasher says:

    I am looking forward to using this. Thanks!

    Do you have any benchmarks on how fast it can process contacts? I know there are a lot of factors like server load, CPU, RAM, etc, but it would be helpful to know how long it took to process your ~500K contacts?

    Like

  12. briancaos says:

    The Contacts will not appear in the MongoDB directly, it is always written to the xDB memory store, and flushed when the session expires.
    Use the following debugging hack to flush your contact to MongoDB:

    Sitecore xDB – flush data to MongoDB

    Like

  13. Pingback: Sitecore MongoDB – What do we know about a contact? | Brian Pedersen's Sitecore and .NET Blog

  14. Pingback: Sitecore contact cannot be loaded, code never responds | Brian Pedersen's Sitecore and .NET Blog

  15. Pingback: Sitecore Web Forms for Marketers – Creating Custom Save Action for Adding Contact Profile for Non-Logged In Visitors – Sitecore Blogs

  16. Shravan says:

    I am writing a batch job where it needs to create multiple contacts, it can go until 100,000. Cant use the session object in my case, i want to save immediately to index as well as mongo and process quickly.

    I am using as sitecore 8.2 update 2. Any suggestion on how to handle this?

    Like

  17. briancaos says:

    Yes, that’s the problem right there. The current xDB implementation are not designed to be able to flush to MongoDB immediately. However, you could try this: Sitecore xDB – flush data to MongoDB.
    BTW, this design flaw is fixed in the upcomming Sitecore 9 implementation called xConnect.

    Like

  18. shravan says:

    To Solve this, I am thinking to create a web API on CD and call that API from CM through batch job. In that CD code – abandon the session after manual updates to contact.

    Is this a good idea, my concern is abandon the session manually in PROD. But i cannot think of anything else, there is no way to update 100,000 records quickly.

    Like

  19. briancaos says:

    The biggest issue is that when using a scheduled job, you have no session to abandon, as your job does not run in the context of a webpage.
    I myself updates 850.000 users on a weekly basis using the code in this article, and I have just gotten used to the fact that the xDB is updated immediately, but the MongoDB is not.
    When using xDB and Contacts, you should accept the fact that your data is not persisted immediately, and be ready to face the issues that might come. I have addressed some of the issues in these articles:
    Sitecore contact cannot be loaded, code never responds
    Sitecore General error when submitting contact – Another contact with the same identifier already exists
    Sitecore xDB – flush data to MongoDB

    Like

  20. Pingback: Add contact to Sitecore contact list using the core pipeline – the experience platform blog

Leave a comment

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