Mark Gibbons
Published on

Fault-resistant Sitecore xConnect client operations with just a few lines of code

Authors

Adding retry logic to your Sitecore xConnect client operations

Have you ever had an issue where an xConnect operation such as updating a contact facet has failed? Why not add some fault tolerance? I’ll show you how!

Example without retry logic

Let’s look at some code of a simple contact save that we might be doing without retry logic:

xConnectClientSubmit.cs
using Newtonsoft.Json;
using Sitecore.Analytics;
using Sitecore.Analytics.Model;
using Sitecore.Analytics.Tracking;
using Sitecore.Diagnostics;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using Sitecore.XConnect.Client.Configuration;
using Sitecore.XConnect.Collection.Model;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Yours
{
    public class ContactRepository : IContactRepository
    {
        private readonly ContactManager _contactmanager;

        public ContactRepository()
        {
            _contactmanager = Sitecore.Configuration.Factory.CreateObject("tracking/contactManager", true) as ContactManager;
        }

        public bool SaveContactData(ContactModel contactModel)
        {
            try
            {
                var contactReference = GetIdentifiedContactReference();

                using (var client = SitecoreXConnectClientConfiguration.GetClient())
                {
                    var contact = client.Get(contactReference, new ContactExpandOptions(PersonalInformation.DefaultFacetKey));

                    // Update your contact facets

                    client.Submit();
                    ReloadContact();
                }
                return true;
            }
            catch (Exception ex)
            {
                Log.Error("Error saving data to profile", ex, this);
                return false;
            }
        }

        private IdentifiedContactReference GetIdentifiedContactReference()
        {
            if (Tracker.Enabled && !Tracker.Current.IsActive)
            {
                Tracker.StartTracking();
            }

            if (Tracker.Current?.Contact == null)
            {
                Log.Warn("Tracker.Current?.Contact == null", this);
                return null;
            }

            if (Tracker.Current.Contact.IsNew)
            {
                Tracker.Current.Contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
                _contactmanager.SaveContactToCollectionDb(Tracker.Current.Contact);
            }

            var id = Tracker.Current.Contact.Identifiers.FirstOrDefault();
            return Tracker.Current.Contact.IsNew || Tracker.Current.Contact.Identifiers.Count == 0
                ? new IdentifiedContactReference(Sitecore.Analytics.XConnect.DataAccess.Constants.IdentifierSource, Tracker.Current.Contact.ContactId.ToString("N"))
                : new IdentifiedContactReference(id.Source, id.Identifier);
        }

        private void ReloadContact()
        {
            if (Tracker.Current?.Session == null)
            {
                Log.Warn("Tracker.Current?.Contact == null", this);
                return;
            }
            _contactmanager.RemoveFromSession(Tracker.Current.Contact.ContactId);
            Tracker.Current.Session.Contact = _contactmanager.LoadContact(Tracker.Current.Contact.ContactId);
        }
    }
}

Source: gist

Add the XdbRequestPerformer

First we’ll add some magic. It provides some exception handling and retry logic which will retry the operation up to 5 times before giving up.

XdbRequestPerformer.cs
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Sitecore.XConnect;
using Sitecore.XConnect.Client.Configuration;
using Sitecore.XConnect.Operations;

namespace Yours
{
    public interface IXdbRequestPerformer
    {
        void RequestWithRetry(Action<IXdbContext> action);
        void RequestWithRetry(Action<IXdbContext> action, string actionMessage);
    }
    public class XdbRequestPerformer : IXdbRequestPerformer
    {
        private const int XdbRequestRetriesCount = 5;

        public void RequestWithRetry(Action<IXdbContext> action)
        {
            RequestWithRetry(action, string.Empty);
        }

        public void RequestWithRetry(Action<IXdbContext> action, string actionMessage)
        {
            using (var xconnectClient = SitecoreXConnectClientConfiguration.GetClient())
            {
                for (int index = 0; index < XdbRequestRetriesCount + 1; ++index)
                {
                    try
                    {
                        action(xconnectClient);
                        break;
                    }
                    catch (Exception ex)
                    {
                        var xdbOperations = xconnectClient?.LastBatch?.Where(x => x.Status == XdbOperationStatus.Failed);
                        var str = string.IsNullOrEmpty(actionMessage) ? string.Empty : "of '" + actionMessage + "'";
                        if (ex is XdbUnavailableException || xdbOperations != null && xdbOperations.Any())
                        {
                            if (index == XdbRequestRetriesCount)
                            {
                                Sitecore.Diagnostics.Log.Error(FormattableString.Invariant(FormattableStringFactory.Create("Request {0} is failed during xDB operation. (Message: {1})", str, ex.Message)), ex, typeof(XdbRequestPerformer));
                                throw;
                            }
                            else
                                Sitecore.Diagnostics.Log.Warn(FormattableString.Invariant(FormattableStringFactory.Create("Request {0} is failed during xDB operation. Trying to retry {1} time... (Message: {2})", str, (index + 1), ex.Message)), ex, typeof(XdbRequestPerformer));
                        }
                        else
                        {
                            Sitecore.Diagnostics.Log.Error(FormattableString.Invariant(FormattableStringFactory.Create("Request {0} is failed. Retry will not be performed because the issue is probably caused not during xDB operation... (Message: {1})", str, ex.Message)), ex, typeof(XdbRequestPerformer));
                            throw;
                        }
                    }
                }
            }
        }
    }
}

Source: gist

Updating the example to use the retry code

With just a few lines of code you can change it to use the XdbRequestPerformer:

xConnectClientSubmitWithXdbRequestPerformer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Sitecore.Analytics;
using Sitecore.Analytics.Model;
using Sitecore.Analytics.Tracking;
using Sitecore.Diagnostics;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using Sitecore.XConnect.Collection.Model;

namespace Yours
{
    public class ContactRepository : IContactRepository
    {
        private readonly ContactManager _contactmanager;
        private readonly XdbRequestPerformer _xdbRequestPerformer;

        public ContactRepository(IXdbRequestPerformer xdbRequestPerformer)
        {
            _contactmanager = Sitecore.Configuration.Factory.CreateObject("tracking/contactManager", true) as ContactManager;
            _xdbRequestPerformer = xdbRequestPerformer;
        }

        public bool SaveContactData(ContactModel contactModel)
        {
            try
            {
                var contactReference = GetIdentifiedContactReference();

                _xdbRequestPerformer.RequestWithRetry(client =>
                {
                    var contact = client.Get(contactReference, new ContactExpandOptions(PersonalInformation.DefaultFacetKey));

                    // Update your contact facets

                    client.Submit();
                    ReloadContact();
                }, "SaveContactData");
                return true;
            }
            catch (Exception ex)
            {
                Log.Error("Error saving data to profile", ex, this);
                return false;
            }
        }

        private IdentifiedContactReference GetIdentifiedContactReference()
        {
            if (Tracker.Enabled && !Tracker.Current.IsActive)
            {
                Tracker.StartTracking();
            }

            if (Tracker.Current?.Contact == null)
            {
                Log.Warn("Tracker.Current?.Contact == null", this);
                return null;
            }

            if (Tracker.Current.Contact.IsNew)
            {
                Tracker.Current.Contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
                _contactmanager.SaveContactToCollectionDb(Tracker.Current.Contact);
            }

            var id = Tracker.Current.Contact.Identifiers.FirstOrDefault();
            return Tracker.Current.Contact.IsNew || Tracker.Current.Contact.Identifiers.Count == 0
                ? new IdentifiedContactReference(Sitecore.Analytics.XConnect.DataAccess.Constants.IdentifierSource, Tracker.Current.Contact.ContactId.ToString("N"))
                : new IdentifiedContactReference(id.Source, id.Identifier);
        }

        private void ReloadContact()
        {
            if (Tracker.Current?.Session == null)
            {
                Log.Warn("Tracker.Current?.Contact == null", this);
                return;
            }
            _contactmanager.RemoveFromSession(Tracker.Current.Contact.ContactId);
            Tracker.Current.Session.Contact = _contactmanager.LoadContact(Tracker.Current.Contact.ContactId);
        }
    }
}

Source: gist

And that’s it!

Another alternative

You could also use Polly to give much the same, with also only a few lines of code. However I think in this particular example it’s going to be easier to use the XdbRequestPerformer which has some additional smarts around batch operations where some operations succeeded and some failed.