Skip to content

Instantly share code, notes, and snippets.

@harshbaid
Forked from digitalParkour/XConnectService.cs
Created December 12, 2019 17:36
Show Gist options
  • Save harshbaid/0e1f12ab4eaf667fc84b79ce160849b0 to your computer and use it in GitHub Desktop.
Save harshbaid/0e1f12ab4eaf667fc84b79ce160849b0 to your computer and use it in GitHub Desktop.
Sitecore XConnectService extension
// ############################
// Example 1: Simple usage
// ############################
// Custom helper:
var xConnectHelper = new XConnectService(new XConnectServiceOperations());
// Or leverage IoC using injection or ServiceLocator:
// var xConnectHelper = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IXConnectService>();
// Set Contact Name
xConnectHelper.SetContactFacet<PersonalInformation>(
facetKey: PersonalInformation.DefaultFacetKey,
identifier: contactIdentifier,
doUpdates: x =>
{
x.FirstName = firstName;
x.LastName = lastName;
}
);
// ############################
// Example 2: Using createNew option to handle special facet instantiation needs
// ############################
// Custom helper:
var xConnectHelper = new XConnectService(new XConnectServiceOperations());
// Or leverage IoC using injection or ServiceLocator:
// var xConnectHelper = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IXConnectService>();
// Set Contact Email
xConnectHelper.SetContactFacet<EmailAddressList>(
facetKey: EmailAddressList.DefaultFacetKey,
identifier: contactIdentifier,
doUpdates: x => {
// Case ignore - Email already saved on contact, do nothing
if (x.PreferredEmail.SmtpAddress.Equals(email, StringComparison.OrdinalIgnoreCase) || x.Others.Any(y => y.Value.SmtpAddress.Equals(email, StringComparison.OrdinalIgnoreCase)))
{
return;
}
// Case contact match - add email
var newEmail = new EmailAddress(email, true);
x.PreferredEmail = newEmail;
},
createNew: () => {
// Case facet does not exist, create it
var newEmail = new EmailAddress(email, true);
return new EmailAddressList(newEmail, "Preferred");
}
);
// ############################
// Example 3: Group multiple facet updates
// ############################
// Custom helper:
var xConnectHelper = new XConnectService(new XConnectServiceOperations());
// Or leverage IoC using injection or ServiceLocator:
// var xConnectHelper = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IXConnectService>();
// Begin set of edits for contact, listing all facet names ahead of time to preload data when getting contact from XConnect.
xConnectHelper.BeginEdit(
contactIdentifier,
PersonalInformation.DefaultFacetKey,
EmailAddressList.DefaultFacetKey,
CustomFacet.DefaultFacetKey,
ListSubscriptions.DefaultFacetKey
);
{ // use code block notation to clarify group of edits, remembering to call EndEdit() afterwards
// Set Contact Name
xConnectHelper.SetContactFacet<PersonalInformation>(
facetKey: PersonalInformation.DefaultFacetKey,
doUpdates: x =>
{
x.FirstName = firstName;
x.LastName = lastName;
}
);
// Set Contact Email, showing createNew option
xConnectHelper.SetContactFacet<EmailAddressList>(
facetKey: EmailAddressList.DefaultFacetKey,
doUpdates: x => {
// Case ignore - Email already saved on contact, do nothing
if (x.PreferredEmail.SmtpAddress.Equals(email, StringComparison.OrdinalIgnoreCase) || x.Others.Any(y => y.Value.SmtpAddress.Equals(email, StringComparison.OrdinalIgnoreCase)))
{
return;
}
// Case contact match - add email
var newEmail = new EmailAddress(email, true);
x.PreferredEmail = newEmail;
},
createNew: () => {
// Case facet does not exist, create it
var newEmail = new EmailAddress(email, true);
return new EmailAddressList(newEmail, "Preferred");
}
);
// Custom facets work the same way
xConnectHelper.SetContactFacet<CustomFacet>(
facetKey: CustomFacet.DefaultFacetKey,
doUpdates: x =>
{
// Save custom form data
x.MyProperty = someValue;
}
);
// Add contact list subscription; another example handling nested objects
xConnectHelper.SetContactFacet<ListSubscriptions>(
facetKey: ListSubscriptions.DefaultFacetKey,
doUpdates: x =>
{
// Ensure object exists
if (x.Subscriptions == null)
{
x.Subscriptions = new List<ContactListSubscription>();
}
// Case already subscribed, do nothing
else if (x.Subscriptions.Any(s => s.ListDefinitionId.Equals(someListId)))
{
return;
}
// Add new contact list subscription
var subscription = new ContactListSubscription(added: DateTime.UtcNow, isActive: true, listDefinitionId: someListId);
x.Subscriptions.Add(subscription);
}
);
}
xConnectHelper.EndEdit(); // submits changes to xConnect
namespace Sitecore.Foundation.SitecoreExtensions.Services
{
using Sitecore.Diagnostics;
using Sitecore.Foundation.DependencyInjection;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using System;
using System.Linq;
/// <summary>
/// Handy, succinct SetContactFacet utility which can be called solo or be stacked and wrapped for multi-facet updates
/// Usages:
/// SetContactFacet();
/// Or:
/// BeginEdit();
/// {
/// SetContactFacet();
/// SetContactFacet();
/// ...
/// }
/// EndEdit();
/// </summary>
public interface IXConnectService
{
void SetContactFacet<TFacet>(string facetKey, Action<TFacet> doUpdates, Func<TFacet> createNew = null, Analytics.Model.Entities.ContactIdentifier identifier = null) where TFacet : Facet;
void BeginEdit(Analytics.Model.Entities.ContactIdentifier targetIdentifier = null, params string[] facetKeys);
void EndEdit();
}
// Uncomment next line if you are using Sitecore.Foundation.DependencyInjection project from habitat
// [Service(typeof(IXConnectService), Lifetime = Lifetime.Transient)]
public class XConnectService : IXConnectService
{
private XConnectClient Client;
private Contact Contact;
private string[] ValidKeys;
private bool IsMultiFacetContext = false;
IXConnectServiceOperations XO;
public XConnectService(IXConnectServiceOperations ops) {
XO = ops;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ========================================================================================================================
/// <summary>
/// Handy, succinct Set Facet Value utility
/// Handles New and Update cases. Works directly with XConnect
/// Reloads WebTracker Cache after editing if target identity exists on current Web Tracker contact
/// Can be used on its own or wrapped between BeginEdit and EndEdit for multiple calls
/// </summary>
/// <typeparam name="TFacet"></typeparam>
/// <param name="facetKey">Key to update</param>
/// <param name="doUpdates">Function to apply update (allows partial)</param>
/// <param name="createNew">If Facet object does not have parameterless constructor, pass in how to instantiate new facet for cases when facet does not yet exist for contact</param>
/// <param name="identifier">Identifier of contact to edit</param>
public virtual void SetContactFacet<TFacet>(string facetKey, Action<TFacet> doUpdates, Func<TFacet> createNew = null, Analytics.Model.Entities.ContactIdentifier targetIdentifier = null)
where TFacet : Facet
{
if (!XO.IsTrackerActive)
{
Log.Warn($"{nameof(XConnectService)}::{nameof(SetContactFacet)} failed, Tracker not active", this);
return;
}
// Extra protection
if (IsMultiFacetContext)
{
Assert.IsTrue(ValidKeys.Contains(facetKey), $"Parameter Facetkey ({facetKey}) does not match list provided from {nameof(BeginEdit)} call");
// TargetIdentifier is not needed for Multi-Facet context, but if it is provided ensure it matches context - maybe catch a usage mistake
if (targetIdentifier != null) {
Assert.IsTrue(Contact?.Identifiers?.Any(
x=> x.Source == targetIdentifier.Source
&& x.Identifier == targetIdentifier.Identifier
) ?? false,
$"{nameof(XConnectService)}::{nameof(SetContactFacet)} Unexpected Case: You specified a contact identifier in a mult-facet context update that did not match the one specified in BeginEdit() call"
);
}
}
// Get contact from xConnect, update and save the facet
if (!IsMultiFacetContext)
{
// When not in multi-facet update context [ie BeginEdit has not been called], must provide Client and Contact here
(var identifier, var trackerIdentifier) = XO.InitializeIdentifiers(targetIdentifier);
Client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient();
Contact = XO.GetOrCreateContact(Client, trackerIdentifier, facetKey);
}
{
try
{
// Apply intended edits
XO.SetFacet(Client, Contact, facetKey, doUpdates, createNew);
}
catch (XdbExecutionException ex)
{
Log.Error($"{nameof(XConnectService)}::{nameof(SetContactFacet)}: Error saving contact facet", ex, this);
throw;
}
}
if (!IsMultiFacetContext)
{
EndEdit();
}
}
// ========================================================================================================================
// \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
/// <summary>
/// Called to start Multi-Facet update context
/// </summary>
/// <param name="targetIdentifier"></param>
/// <param name="facetKeys"></param>
public void BeginEdit(Analytics.Model.Entities.ContactIdentifier targetIdentifier = null, params string[] facetKeys)
{
ValidKeys = facetKeys;
IsMultiFacetContext = true;
Client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient();
(var identifier, var trackerIdentifier) = XO.InitializeIdentifiers(targetIdentifier);
Contact = XO.GetOrCreateContact(Client, trackerIdentifier, facetKeys);
}
/// <summary>
/// Called to commit Multi-Facet update
/// Also called internally for single facet updates
/// </summary>
public void EndEdit()
{
Client.Submit();
// Case to reload Web Tracker data
if (Sitecore.Analytics.Tracker.Current?.Contact?.ContactId.Equals(Contact.Id) ?? false)
{
XO.ReloadContactFacets();
}
Client?.Dispose();
// reset
IsMultiFacetContext = false;
Contact = null;
}
}
}
namespace Sitecore.Foundation.SitecoreExtensions.Services
{
using Sitecore.Analytics;
using Sitecore.Analytics.Model;
using Sitecore.Diagnostics;
using Sitecore.Foundation.DependencyInjection;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using System;
using System.Linq;
/// <summary>
/// Set of helper methods to consolidate code
/// </summary>
public interface IXConnectServiceOperations
{
bool IsTrackerActive { get; }
void SetFacet<TFacet>(XConnectClient client, Contact contact, string facetKey, Action<TFacet> doUpdates, Func<TFacet> createNew = null)
where TFacet : Facet;
Contact GetOrCreateContact(XConnectClient client, IdentifiedContactReference trackerIdentifier, params string[] facetKeys);
void CommitCurrentContact();
void ReloadContactFacets();
(Analytics.Model.Entities.ContactIdentifier identifier, IdentifiedContactReference reference) InitializeIdentifiers(Analytics.Model.Entities.ContactIdentifier identifier);
}
// Uncomment next line if you are using Sitecore.Foundation.DependencyInjection project from habitat
// [Service(typeof(IXConnectServiceOperations), Lifetime = Lifetime.Singleton)]
public class XConnectServiceOperations : IXConnectServiceOperations
{
public virtual bool IsTrackerActive => Tracker.Enabled && Tracker.Current != null && Tracker.Current.IsActive;
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ========================================================================================================================
public virtual void SetFacet<TFacet>(XConnectClient client, Contact contact, string facetKey, Action<TFacet> doUpdates, Func<TFacet> createNew = null)
where TFacet : Facet
{
// Parameter defaults:
if (createNew == null)
{ // Default to empty constructor
createNew = () => (TFacet)Activator.CreateInstance(typeof(TFacet));
}
// Case - new facet
var facet = contact.Facets.ContainsKey(facetKey) ?
((TFacet)contact.Facets[facetKey])
: createNew();
// Do work - update facet
doUpdates(facet);
// Apply
client.SetFacet<TFacet>(contact, facetKey, facet);
}
// ========================================================================================================================
// \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
/// <summary>
/// Get or create the contact from XConnect
/// Load existing facet data for contact if exists
/// </summary>
/// <param name="client"></param>
/// <param name="trackerIdentifier"></param>
/// <param name="facetKeys"></param>
/// <returns></returns>
public virtual Contact GetOrCreateContact(XConnectClient client, IdentifiedContactReference trackerIdentifier, params string[] facetKeys)
{
// Get existing contact (load current facet data to support partial edits)
var contact = client.Get<XConnect.Contact>(trackerIdentifier, new Sitecore.XConnect.ContactExpandOptions(facetKeys));
// Case - new contact
if (contact == null)
{
contact = new Sitecore.XConnect.Contact(
new Sitecore.XConnect.ContactIdentifier(trackerIdentifier.Source, trackerIdentifier.Identifier, Sitecore.XConnect.ContactIdentifierType.Known)
);
client.AddContact(contact); // Extension found in Sitecore.XConnect.Operations
}
return contact;
}
/// <summary>
/// Commit current contact from session Web Tracker to XConnect app
/// </summary>
public virtual void CommitCurrentContact()
{
if (!IsTrackerActive)
{
Log.Warn($"{nameof(XConnectServiceOperations)}::{nameof(CommitCurrentContact)} failed, Tracker not active", nameof(XConnectServiceOperations));
return;
}
var manager = Sitecore.Configuration.Factory.CreateObject("tracking/contactManager", true) as Sitecore.Analytics.Tracking.ContactManager;
if (manager != null)
{
// Save contact to xConnect; at this point, a contact has an anonymous
// TRACKER IDENTIFIER, which follows a specific format. Do not use the contactId overload
// and make sure you set the ContactSaveMode as demonstrated
Sitecore.Analytics.Tracker.Current.Contact.ContactSaveMode = ContactSaveMode.AlwaysSave;
manager.SaveContactToCollectionDb(Sitecore.Analytics.Tracker.Current.Contact);
}
}
/// <summary>
// When editing XConnect directly, Sitecore Web Tracker Session data will be stale and needs to be reloaded.
// Remove contact data from shared session state - contact will be re-loaded during subsequent request with updated facet data
/// </summary>
public virtual void ReloadContactFacets()
{
if (!IsTrackerActive)
{
Log.Warn($"{nameof(XConnectServiceOperations)}::{nameof(ReloadContactFacets)} failed, Tracker not active", nameof(XConnectServiceOperations));
return;
}
var manager = Sitecore.Configuration.Factory.CreateObject("tracking/contactManager", true) as Sitecore.Analytics.Tracking.ContactManager;
manager.RemoveFromSession(Sitecore.Analytics.Tracker.Current.Contact.ContactId);
Sitecore.Analytics.Tracker.Current.Session.Contact = manager.LoadContact(Sitecore.Analytics.Tracker.Current.Contact.ContactId);
}
/// <summary>
/// Condense logic to handle identifier since it is needed for single and multipe facet edit context
/// </summary>
/// <param name="identifier"></param>
/// <returns></returns>
public virtual (Analytics.Model.Entities.ContactIdentifier identifier, IdentifiedContactReference reference) InitializeIdentifiers(Analytics.Model.Entities.ContactIdentifier identifier)
{
if (identifier == null)
{ // Default to first known identifier
identifier = Sitecore.Analytics.Tracker.Current.Contact.Identifiers.FirstOrDefault(x => x.Type == ContactIdentificationLevel.Known);
if (identifier == null)
{ // Otherwise just first identifier
identifier = Sitecore.Analytics.Tracker.Current.Contact.Identifiers.FirstOrDefault();
}
}
// Case new contact with matching identifier
// ... contact must exist in XConnect... we can add a new one if needed, but if current contact matches, then ensure this one is in xConnect so we pick it next
if (Sitecore.Analytics.Tracker.Current.Contact.IsNew
&& Sitecore.Analytics.Tracker.Current.Contact.Identifiers.Contains(identifier))
{
CommitCurrentContact();
}
var trackerIdentifier = identifier == null ?
new IdentifiedContactReference(Sitecore.Analytics.XConnect.DataAccess.Constants.IdentifierSource, Sitecore.Analytics.Tracker.Current.Contact.ContactId.ToString("N")) :
new IdentifiedContactReference(identifier.Source, identifier.Identifier);
return (identifier, trackerIdentifier);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment