Sitecore SVG files

In Sitecore 8.1, there is a tiny but annoying glitch in the matrix, where SVG files are not allowed in the Media Library.

But do not worry, the fix is easy. Go to your /App_Config/Sitecore.config, find the /sitecore/mediaLibrary/mediaTypes/mediaType section, and add the SVG section yourself:

<mediaType name="SVG" extensions="svg">
  <mimeType>image/svg+xml</mimeType>
  <forceDownload>false</forceDownload>
  <sharedTemplate>system/media/unversioned/image</sharedTemplate>
  <versionedTemplate>system/media/versioned/image</versionedTemplate>
  <mediaValidator type="Sitecore.Resources.Media.ImageValidator"/>
  <thumbnails>
    <generator type="Sitecore.Resources.Media.ImageThumbnailGenerator, Sitecore.Kernel">
      <extension>png</extension>
    </generator>
    <width>150</width>
    <height>150</height>
    <backgroundColor>#FFFFFF</backgroundColor>
  </thumbnails>
</mediaType>

MORE TO READ:

Posted in Sitecore, Sitecore 8 | Tagged , | 1 Comment

Sitecore EXM: Send an email from code

The Sitecore Email Experience Manager is your way to send personalized emails to Sitecore users.   You do not need to send bulk emails, you can easily send single emails with contents like “Here is your new password” or “Your profile has been updated”.

The emails to send are “Standard Messages” so the email you create must be of the type “Triggered message”:

Triggered Message Settings

Triggered Message Settings

There is 2 ways of sending emails: To Sitecore users or Sitecore Contacts.

SEND AN EMAIL TO A SITECORE USER

Your Sitecore user must exist in the Sitecore User repository (usually as an “extranet” user).

using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Messages;
using Sitecore.Modules.EmailCampaign.Recipients;

public void Send(ID messageItemId, string userName)
{
  MessageItem message = Factory.GetMessage(messageItemId);
  Assert.IsNotNull(message, "Could not find message with ID " + messageItemId);
  RecipientId recipient = new SitecoreUserName(userName);
  Assert.IsNotNull(recipient, "Could not find recipient with username " + userName);
  new AsyncSendingManager(message).SendStandardMessage(recipient);
}

You call the function like this:

Send(new ID("{12A6D766-CA92-4303-81D2-57C66F20AB12}"), "extranet\\user@domain.com");

SEND AND EMAIL TO A CONTACT

The contact must (obviously) contain an email address. To create a contact see Sitecore Contacts – Create and save contacts to and from xDB (MongoDB). The code is near identical to the previous, but the Receipient is retrieved by resolving the contact ID:

using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Messages;
using Sitecore.Modules.EmailCampaign.Recipients;

public void Send(ID messageItemId, Guid contactID)
{
  MessageItem message = Factory.GetMessage(messageItemId);
  Assert.IsNotNull(message, "Could not find message with ID " + messageItemId);
  RecipientId recipient = RecipientRepository.GetDefaultInstance().ResolveRecipientId("xdb:" + contactID);
  Assert.IsNotNull(recipient, "Could not find recipient with ID " + contactID);
  new AsyncSendingManager(message).SendStandardMessage(recipient);
}

You call the function like this:

Send(new ID("{12A6D766-CA92-4303-81D2-57C66F20AB12}"), Guid.Parse("c3b8329b-7930-405d-8852-7a88ef4f0cb1"));

MORE TO READ:

 

Posted in c#, General .NET, Sitecore 8 | Tagged , , , , , | Leave a comment

Sitecore 8 EXM: Failed to enroll a contact in the engagement plan

In Sitecore 8.1 update 1, you might experience the following error when trying to send an email:

ERROR Failed to enroll a contact in the engagement plan.
Exception: System.Net.WebException
Message: The remote name could not be resolved: ‘default-cd-cluster’


Message: Recipient sc:extranet\someemail@pentia.dk skipped. Failed to enroll its corresponding contact in the engagement plan.
Source: Sitecore.EmailCampaign
at Sitecore.Modules.EmailCampaign.Core.Dispatch.DispatchTask.OnSendToNextRecipient()

The error occurs if you do not change the Analytics.ClusterName in the /App_Config/Include/Sitecore.Analytics.Tracking.config.

<setting name="Analytics.ClusterName" value="default-cd-cluster" />

The Analytics.ClusterName must be a valid, reachable URL.

<setting name="Analytics.ClusterName" value="hostname.of.my.site.com" />

This is because Sitecore calls the /sitecore/AutomationStates.ashx page using the Analytics.ClusterName as the host name.

Posted in Sitecore 8 | Tagged , , , , | Leave a comment

Sitecore xDB – flush data to MongoDB

When debugging Sitecore xDB issues, it is a pain that data is not written to MongoDB directly, but you have to wait for your session to end.

The legend said that you could either set the session timeout to 1 minute, or call Session.Abandon() to write to MongoDB. None of this have ever worked for me, until my colleague Lasse Pedersen found the final, bullet-proof way of having the xDB session data written to MongoDB.

Create a page with the following contents, and call it when you wish to flush data from xDB to MongoDB:

Sitecore.Analytics.Tracker.Current.EndTracking();
HttpContext.Current.Session.Abandon();

We will all pray to the debugging gods that this fix will work in every scenario.

Posted in c#, General .NET, Sitecore 8 | Tagged , , , | 3 Comments

Why is session NULL in BeginRequest()? (httpRequestBegin)

Have you ever wondered why the session is NULL when a request begins?
The answer is simple: The Application_BeginRequest() event is fired before the Session_Start() event.
The BeginRequest() event is fired for each request as the first event. The Session_Start() event is only fired once per session, after the BeginRequest() event.

The following is specific for Sitecore:

This means that if you add a new processor in the httpRequestBegin pipeline, you will get an exception if you call HttpContext.Current.Session:

namespace MyNameSpace
{
  class MyFunction : HttpRequestProcessor
  {
    public override void Process(HttpRequestArgs args)
    {
	  // THIS WILL FAIL:
	  var somevar = HttpContext.Current.Session;
    }
  }
}

Simply because the session is not available at the time.

So what do you do then?

Sitecore have added another pipeline, httpRequestProcessed, which is fired AFTER httpRequestBegin AND Session_Start():

<httpRequestProcessed>
  <processor type="MyNamespace.MyFunction, MyDLL" />
</httpRequestProcessed>

This pipeline is usually used for replacing content or setting http status codes after the contents of a website have been processed, but you can use it for any purpose you require.

And please note that the pipeline is called for every request that have ended, so you need the same amount of carefulness as when calling httpRequestBegin. Remember to filter out doublets, Sitecore calls, check for Sitecore context etc., before calling your code.

// This is a classic (maybe a little over-the-top) 
// test to see if the request is a request
// that needs processing
namespace MyNamespace
{
  public class MyClass : HttpRequestProcessor
  {
    public override void Process(HttpRequestArgs args)
    {
      Assert.ArgumentNotNull(args, "args");
      if (Context.Site == null)
        return;
      if (Context.Site.Domain != DomainManager.GetDomain("extranet"))
        return;
      if (args.Url.FilePathWithQueryString.ToUpperInvariant().Contains("redirected=true".ToUpperInvariant()))
        return; // already redirected
      if (Context.PageMode.IsPageEditor)  
	    return;

      // DO CODE
    }
  }
}

MORE TO READ:

 

Posted in General .NET, Sitecore 6, Sitecore 7, Sitecore 8 | Tagged , , , , , , | Leave a comment

Sitecore sublayout caching vary by cookie

In Sitecore you can control the caching of your sublayouts in many different ways.

Caching parameters

Caching parameters

Checking the “Cacheable” box allows you you vary the cache by data (data source), device, login (anonymous users have a different cached version from named users), param (item parameters), query string or user (each user has his own version).

Caching is an area that hasn’t changed much since Sitecore 5, which is why this code should work on any Sitecore version. However, For caching in MVC, Sitecore have implemented a different approach. See CUSTOM CACHE CRITERIA WITH MVC IN THE SITECORE ASP.NET CMS by John West for more information.

But what if you wish to vary the cache on another parameter, say, the contents of a cookie?

My colleague, Niclas Awalt, solved this for me a while ago.

STEP 1: SETTING UP SITECORE

First we need to define the name of the cookie to vary by. This is done in the “Parameters” field on the sublayout:

Parameters

The “VaryByCookie” defines my caching key, and the “Mycookie” defines the cookie to vary by.

STEP 2: CREATE A CUSTOM SUBLAYOUT

This custom sublayout contains the code that determines if the sublayout is cacheable, and if the “VaryByCookie” value is set, and if so, creates a new cache key based on the contents of the cookie:

using System.Collections.Specialized;
using System.Text;

namespace MyNamespace
{
  public class CustomSublayout : Sitecore.Web.UI.WebControls.Sublayout
  {
    private string _varyByCookies;
    private string VaryByCookies
    {
      get
      {
        return _varyByCookies ?? (_varyByCookies = GetVaryByCookiesParameter());
      }
    }

    public override string GetCacheKey()
    {
      Sitecore.Sites.SiteContext site = Sitecore.Context.Site;

      if (!Cacheable)
        return base.GetCacheKey();

      if (site != null && (!site.CacheHtml))
        return base.GetCacheKey();

      if (SkipCaching())
        return base.GetCacheKey();

      if (string.IsNullOrEmpty(VaryByCookies))
        return base.GetCacheKey();

      var cacheKey = base.GetCacheKey() + BuildCookieValueString();
      return cacheKey;
    }

    private string BuildCookieValueString()
    {
      StringBuilder stringBuilder = new StringBuilder();
      foreach (var cookieName in VaryByCookies.Split('|'))
      {
        stringBuilder.Append(GetCookieValue(cookieName, "0NoValue0"));
      }
      if (stringBuilder.Length > 0)
        stringBuilder.Insert(0, "_#cookies:");
      return stringBuilder.ToString();
    }

    private string GetCookieValue(string cookieName, string defaultValue)
    {
      var cookieValue = Context.Request.Cookies[cookieName];
      return string.Concat(cookieName, cookieValue == null ? defaultValue : cookieValue.Value);
    }

    private string GetVaryByCookiesParameter()
    {
      NameValueCollection parameters =
        Sitecore.Web.WebUtil.ParseUrlParameters(this.Parameters);

      return parameters["VaryByCookie"] ?? string.Empty;
    }
  }
}

STEP 3: HOOK UP THE NEW SUBLAYOUT:

Next step is to replace the Sitecore Sublayout rendering control with our own.

First, create a small rendering type class:

using System.Collections.Specialized;
using System.Web.UI;

namespace MyNamespace
{
  public class CustomSublayoutRenderingType : Sitecore.Web.UI.SublayoutRenderingType
  {
    public override Control GetControl(NameValueCollection parameters, bool assert)
    {
      CustomSublayout sublayout = new CustomSublayout();

      foreach (string name in parameters.Keys)
      {
        string str = parameters[name];
        Sitecore.Reflection.ReflectionUtil.SetProperty(sublayout, name, str);
      }

      return sublayout;
    }
  }
}

Then, in the web.config, in the <renderingControls> section, call our new sublayout rendering control:

<renderingControls>
  <control template="method rendering" type="Sitecore.Web.UI.WebControls.Method, Sitecore.Kernel" propertyMap="AssemblyName=assembly, ClassName=class, MethodName=method" />
  <!-- OUR NEW RENDERING -->
  <control template="sublayout" type="MyNamespace.CustomSublayoutRenderingType, MyDll" propertyMap="Path=path" />
  <!-- OUR NEW RENDERING -->
  <control template="url rendering" type="Sitecore.Web.UI.WebControls.WebPage, Sitecore.Kernel" propertyMap="Url=url" />
  <control template="xsl rendering" type="Sitecore.Web.UI.XslControlRenderingType, Sitecore.Kernel" propertyMap="Path=path" />
  <control template="webcontrol" type="Sitecore.Web.UI.WebControlRenderingType, Sitecore.Kernel" propertyMap="assembly=assembly, namespace=namespace, class=tag, properties=parameters" />
  <control template="xmlcontrol" type="Sitecore.Web.UI.XmlControlRenderingType, Sitecore.Kernel" propertyMap="controlName=control name, properties=parameters" />
</renderingControls>

Now, each time you create a sublayout, it will automatically be able to use the VaryByCookie parameter, and vary the cache based on the contents of the cookie.

MORE TO READ:

Posted in c#, General .NET, Sitecore, Sitecore 5, Sitecore 6, Sitecore 7, Sitecore 8 | Tagged , , , | 1 Comment

Azure CloudQueue, Get and Set Json using Newtonsoft.Json and Microsoft.WindowsAzure.Storage

The Microsoft Azure Cloud have a nice simple Queue mechanism in their Azure Storage where you can store up to 64 kb of data for up to 7 days. The queue is very easy to set up and very easy to work with when using C#.

The Azure Storage Queue stores simple text strings, but with Newtonsoft.Json you can serialize and deserialize Json messages and by that create a Json based message queue system.

To use the Azure Storage Queue you need to set up an account. Click here to see how an account is made. Creating the account creates a key that can be concatenated to a connection string of the following format:

DefaultEndpointsProtocol=https;
AccountName=YOURACCOUNTNAME;
AccountKey=A_VERY_LONG_ACCOUNT_KEY

With this connection string you can access the queues you made in the system.

STEP 1: CREATE A GENERIC REPOSITORY TO GET AND SET OBJECTS

This repository allows you to get and set messages to and from the Queue.

using System.Collections.Generic;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using Newtonsoft.Json;

namespace MyNamespace
{
  public class AzureQueueStorageRepository
  {
    private readonly CloudQueue _queue;

    /// Create an instance of [see cref="AzureQueueStorageRepository"]½
    /// <param name="queueName">Name of queue</param>
    /// <param name="connectionString">Azure Connectionstring</param>
    public AzureQueueStorageRepository(string queueName, string connectionString)
    {
      Assert.ArgumentNotNullOrEmpty(queueName, "queueName");
      
      CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
      CloudQueueClient client = storageAccount.CreateCloudQueueClient();
      _queue = client.GetQueueReference(queueName);
      _queue.CreateIfNotExists();
    }

    /// Retrieves number of messages from queue. Messages are removed from queue when retrieved
    /// <param name="count">Number of messages to retrieve</param>
    public IEnumerable<CloudQueueMessage> GetMessages(int count)
    {
      int batchRetrieveCount = 32; //max batchsize
      var retrievedMessages = new List<CloudQueueMessage>();
      do
      {
        if (count < 32)
          batchRetrieveCount = count;
        IEnumerable<CloudQueueMessage> receivedBatch = _queue.GetMessages(batchRetrieveCount, TimeSpan.FromHours(48));
        retrievedMessages.AddRange(receivedBatch);
        DeleteBatch(receivedBatch);
      } while ((count -= batchRetrieveCount) > 0);
      return retrievedMessages;
    }

    /// Peek one message from the queue. The message is not removed from queue, nor marked as invisible.
    /// <returns>Peeked message</returns>
    public CloudQueueMessage Peek()
    {
      return _queue.GetMessage(TimeSpan.MinValue);
    }

    ///
    /// Delete one message from the queue
    /// <param name="message">Message to delete</param>
    public void Delete(CloudQueueMessage message)
    {
      _queue.DeleteMessageAsync(message);
    }

    /// Add message to queue
    /// <param name="messageObject">Message to add</param>
    public void AddMessage(object messageObject)
    {
      string serializedMessage = JsonConvert.SerializeObject(messageObject);
      CloudQueueMessage cloudQueueMessage = new CloudQueueMessage(serializedMessage);
      _queue.AddMessageAsync(cloudQueueMessage);
    }

    private void DeleteBatch(IEnumerable<CloudQueueMessage> batch)
    {
      foreach (CloudQueueMessage message in batch)
      {
        _queue.DeleteMessageAsync(message);
      }
    }
  }
}

The repository itself does not know which Json messages you get or set, which is why the GetMessages() returns the Microsoft.WindowsAzure.Storage.Queue.CloudQueueMessage and the AddMessage() inserts an untyped object.

STEP 2: HOW TO ADD AN OBJECT TO THE QUEUE AS A JSON STRING

To add an object to the Queue as a Json message you need to have an object, serialize it to Json and then store this object. Here is a sample object than can be serialized to Json:

using Newtonsoft.Json;

namespace MyNamespace
{
  public class MyMessage
  {
    [JsonProperty("propertyString", Required = Required.Always)]
    public string PropertyString { get; set; }

    [JsonProperty("propertyInt", Required = Required.Always)]
    public int propertyInt { get; set; }
  }
}

To store this message in the Azure Storage Queue you can do the following:

var message = new Mymessage();
message.PropertyString = "Hello World";
message.propertyInt = "2015";

var rep = new AzureQueueStorageRepository("queuename", "connectionstring");
rep.AddMessage(message);

STEP3: HOW TO GET AN JSON STRING FROM THE QUEUE AND DESERIALZE IT TO AN OBJECT

To get the message from Azure Storage Queue you can do the following:

The repository returns a CloudQueueMessage that contains the Json as a text string. This string needs to be serialized into an object. To help with this I have created a helper method that I can call for all my deserialization needs:

/// Deserializes JSON into object of type T.
/// <typeparam name="T">object of type T to deserialize to</typeparam>
/// <param name="json">JSON string</param>
/// <param name="ignoreMissingMembersInObject">If [true] the number of fields in JSON does not have to match the number of properties in object T</param>
/// <returns>object of type T</returns>
public T Deserialize<T>(string json, bool ignoreMissingMembersInObject) where T : class
{
  T deserializedObject;
  try
  {
	MissingMemberHandling missingMemberHandling = MissingMemberHandling.Error;
	if (ignoreMissingMembersInObject)
	  missingMemberHandling = MissingMemberHandling.Ignore;
	deserializedObject = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
	{
	  MissingMemberHandling = missingMemberHandling,
	});
  }
  catch (JsonSerializationException)
  {
	return null;
  }
  return deserializedObject;
}

With this helper class, deserialization is easy:

var rep = new AzureQueueStorageRepository("queuename", "connectionstring");
IEnumerable<CloudQueueMessage> messages = azureQueueRepository.GetMessages(1);
foreach (CloudQueueMessage message in messages)
{
  string jsonString = message.AsString;
  MyMessage myMessage = Deserialize<MyMessage>(jsonString, true);
  
  // do with the object whatever you need

}

MORE TO READ:

Posted in .net, c#, General .NET | Tagged , , , , | Leave a comment

Sitecore List Manager – Add Contacts to EXM Lists

This is yet another post on the focal point of Sitecore 8: Contacts. The contact repository is a multi-usage storage of user information, from visitors (named and anonymous) to imported email addresses to be used in email campaigns.

The Sitecore List Manager is a part of EXM (Email Experience Manager) and replaces .NET security roles as segmentation. In Sitecore 8, you do not need a .NET user to send a mail to a person, all you need is a Contact.

To send an email to more than one person you create a list, add Contacts, create an email, select the list and send the email.

My lists - oh what a mess. Don't worry, it's just test data.

My lists – oh what a mess. Don’t worry, it’s just test data.

A ContactList is no more than a Facet on the Contact. The facet contains the GUID of the Contact List Item and a timestamp:

ContactList Facet

ContactList Facet

So because the list is not actually a list but a set of unconnected GUID’s on unconnected Contacts, it is very easy to add and remove users from Contacts (thanks to Michael Sundstrøm for the code):

using System.Collections.Generic;
using Sitecore.Analytics.Model.Entities;
using Sitecore.Analytics.Tracking;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.SecurityModel;

namespace MyNamespace
{
  public class ContactListRepository
  {
    private const string _CONTACT_LIST_TAG_NAME = "ContactLists";

    public IEnumerable<Item> Get(Contact contact, Database database)
    {
      ITag tag = contact.Tags.Find(_CONTACT_LIST_TAG_NAME);

      if (tag == null)
        yield break;

      foreach (ITagValue tagValue in tag.Values)
      {
        yield return database.GetItem(tagValue.Value);
      }
    }

    public void Add(Contact contact, Item listItem)
    {
      using (new SecurityDisabler())
      {
        contact.Tags.Set(_CONTACT_LIST_TAG_NAME, listItem.ID.ToString());
      }
    }

    public void Remove(Contact contact, Item listItem)
    {
      using (new SecurityDisabler())
      {
        contact.Tags.Remove(_CONTACT_LIST_TAG_NAME, listItem.ID.ToString());
      }
    }

  }
}

So how to Sitecore make these unconnected GUID’s into lists? Each time you add data to the xDB (MongoDB), Sitecore updates the index, Lucene.NET or SOLR. Data is also always queried through the index which is why Sitecore does not need a separate List collection in MongoDB.

MORE TO READ:

Posted in c#, Sitecore 8 | Tagged , , , , | 2 Comments

Sitecore Virtual Users – authenticate users from external systems

One of the very old Sitecore features popped up yesterday. My team and I are working on moving our 550.000+ users from the .NET Membership Provider to an external system, possibly .NET Identity.

.NET Identity will authorize the users, but we still need a Sitecore Membership User for authentication. To do this job, Sitecore created the Virtual User. A Virtual User is in effect a one-time, memory only Sitecore User than can be created on the fly without the use of a password.

Once the user have been authorized (username/password matches) by the external system, we can create a virtual user that Sitecore will recognize as a normal user:

// Create virtual user
var virtualUser = Sitecore.Security.Authentication.AuthenticationManager.BuildVirtualUser("extranet\\user@domain.com", true);

// You can add roles to the Virtual user
virtualUser.Roles.Add(Sitecore.Security.Accounts.Role.FromName("extranet\\MyRole"));

// You can even work with the profile if you wish
virtualUser.Profile.SetCustomProperty("CustomProperty", "12345");
virtualUser.Profile.Email = "user@domain.com";
virtualUser.Profile.Name = "My User";

// Login the virtual user
Sitecore.Security.Authentication.AuthenticationManager.LoginVirtualUser(virtualUser);

After the user have been authenticated using the LoginVirtualUser function, Sitecore will assume the identity of this user:

// This will return TRUE
Sitecore.Context.User.IsAuthenticated;

// This will return "extranet\user@domain.com"
Sitecore.Context.User.Name;

// This will return "My user"
Sitecore.Context.User.Profile.Name;

// This will return "1"
Sitecore.Context.User.Roles.Count;

// This will return "12345"
Sitecore.Context.User.Profile.GetCustomProperty("CustomProperty");

Please note that Sitecore states that if you use the User.Profile, they cannot guarantee that they will not write to the ASP.Net Membership database, although this cannot be confirmed with my current version 8.0 of Sitecore. I did not get anything written to my CORE database.

MORE TO READ:

Posted in c#, Sitecore, Sitecore 5, Sitecore 6, Sitecore 7, Sitecore 8 | Tagged , , , | Leave a comment

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.

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
  {
    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);
          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.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:

 

Posted in .net, c#, General .NET, Sitecore 8 | Tagged , , , , , , , , | 3 Comments