Last active
December 12, 2019 17:36
-
-
Save digitalParkour/07051e01fd46fc9836a057617c1ec1d7 to your computer and use it in GitHub Desktop.
Sitecore XConnectService extension
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ############################ | |
// 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; | |
} | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ############################ | |
// 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"); | |
} | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ############################ | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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