博客三部曲:
- https://sitecorewithraman.wordpress.com/2020/08/02/sitecore-xconnect-custom-facets-part-i/
- https://sitecorewithraman.wordpress.com/2021/03/27/customize-list-manager-part-ii/
- https://sitecorewithraman.wordpress.com/2021/07/01/customize-list-manager-part-iii/
開發
新建 facet model
[Serializable]
[FacetKey(DefaultFacetKey)]
public class Subscription : Facet
{
public string SubscriptionDate { get; set; }
public const string DefaultFacetKey = "Subscriptions";
}
public static class SubscriptionCollectionModel
{
public static XdbModel Model { get; } = CreateModel();
private static XdbModel CreateModel()
{
XdbModelBuilder builder = new XdbModelBuilder("LinkReit.CustomFacets.XConnect.SubscriptionCollectionModel", new XdbModelVersion(1, 0));
builder.ReferenceModel(Sitecore.XConnect.Collection.Model.CollectionModel.Model);
builder.DefineFacet<Contact, Subscription>(Subscription.DefaultFacetKey);
return builder.BuildModel();
}
}
新建 Mapper 和 DataReader
class SubscriptionFacetMapper : IFacetMapper
{
private const string FacetMapperPrefix = "Subscriptions_";
public SubscriptionFacetMapper() : this("Subscriptions")
{
}
public SubscriptionFacetMapper(string facetName)
{
Assert.ArgumentNotNull(facetName, nameof(facetName));
FacetName = facetName;
}
public string FacetName { get; }
public MappingResult Map(string facetKey, Facet facet, ContactMappingInfo mappings, string[] data)
{
using (EventLog eventLog = new EventLog("Application"))
{
eventLog.Source = "Application";
eventLog.WriteEntry($"SubscriptionFacetMapperCalled facetKey:{facetKey} data{string.Join("; ", data)}");
}
if (facetKey != FacetName)
return new NoMatch(facetKey);
string format = "yyyy-MM-dd HH:mm:sszzz";
Subscription subscription = new Subscription();
string subscriptionDate = mappings.GetValue(FacetMapperPrefix + nameof(subscription.SubscriptionDate), data);
//Log.Info($"Facet Mapper: {FacetMapperPrefix}{nameof(subscription.SubscriptionDate)}", this);
if (!string.IsNullOrEmpty(subscriptionDate) && DateTime.TryParse(subscriptionDate, out DateTime result))
subscription.SubscriptionDate = result.ToString(format);
else
subscription.SubscriptionDate = DateTime.Now.ToString(format);
return new FacetMapped(facetKey, subscription);
}
}
class SubscriptionContactDataReader : IContactDataReader
{
public string FacetName
{
get
{
return "Subscriptions";
}
}
public string Map(Contact contact)
{
string SubscriptionFacetKey = Subscription.DefaultFacetKey;
return contact.GetFacet<Subscription>(SubscriptionFacetKey)?.SubscriptionDate ?? "";
}
}
新建自己的 Store
public interface ICustomListSubscriptionsStore<T> where T : new()
{
IEnumerable<T> GetSubscribers(Guid listId, string searchFilter, int pageIndex, int pageSize);
void Unsubscribe(Guid listId, Guid contactId);
void CreateSubscribedContact(Guid listId, SubscriptionContactDataModel contactDataModel);
}
class CustomListSubscriptionsStore : ICustomListSubscriptionsStore<SubscriptionContactDataModel>
{
private static readonly Regex SourceValidator = new Regex("^[a-zA-Z0-9.\\-_]+$", RegexOptions.Compiled);
private readonly IContactProvider _contactProvider;
private readonly IContactListProvider _contactListProvider;
private readonly IContactSourceFactory _contactSourceFactory;
private readonly ISegmentationService _segmentationService;
private readonly IListOperationRepository _operationRepository;
private readonly ISubscriptionService _subscriptionService;
private readonly int _batchSize;
public CustomListSubscriptionsStore(IContactListProvider contactListProvider, ISubscriptionService subscriptionService, IContactProvider contactProvider, IListOperationRepository operationRepository, int BatchSize, IContactSourceFactory csf, ISegmentationService sss)
{
Assert.ArgumentNotNull(contactListProvider, "contactListProvider");
Assert.ArgumentNotNull(subscriptionService, "subscriptionService");
Assert.ArgumentNotNull(contactProvider, "contactProvider");
Assert.ArgumentNotNull(operationRepository, "operationRepository");
Assert.ArgumentNotNull(BatchSize, "settings");
Assert.ArgumentNotNull(csf, "contactSourceFactory");
Assert.ArgumentNotNull(sss, "segmentationService");
_contactListProvider = contactListProvider;
_subscriptionService = subscriptionService;
_contactProvider = contactProvider;
_operationRepository = operationRepository;
_batchSize = BatchSize;
_contactSourceFactory = csf;
_segmentationService = sss;
}
public IEnumerable<SubscriptionContactDataModel> GetSubscribers(Guid listId, string searchFilter, int pageIndex, int pageSize)
{
using (new Sitecore.SecurityModel.SecurityDisabler())
{
try
{
ContactList contactList = EnsureList(listId, null);
ContactSearchResult result = GetFilteredContacts(contactList, searchFilter, pageIndex, pageSize);
List<SubscriptionContactDataModel> source = result.Contacts.Select(new Func<Contact, SubscriptionContactDataModel>(MapEntity)).ToList();
if (source.Any())
source[0].Count = result.Count;
return source;
}
catch (Exception ex)
{
Log.Error(ex.ToString(), ex, this);
}
return null;
}
}
private ContactList EnsureList(Guid listId, string message = null)
{
ContactList val = _contactListProvider.Get(listId, Context.Language.CultureInfo);
if (val != null)
return val;
if (message != null)
throw new ContactListNotFoundException(message);
throw new ContactListNotFoundException(listId);
}
private SubscriptionContactDataModel MapEntity(Contact entity)
{
SubscriptionContactDataModel model1 = new SubscriptionContactDataModel();
Guid? id = entity.Id;
model1.Id = (id.HasValue ? id.GetValueOrDefault() : Guid.Empty).ToString();
model1.Email = (entity.Emails()?.PreferredEmail == null) ? null : (entity.Emails()?.PreferredEmail).SmtpAddress;
model1.FirstName = entity.Personal()?.FirstName;
model1.LastName = entity.Personal()?.LastName;
model1.SubscriptionDate = "";
if (entity.GetFacet<Subscription>(Subscription.DefaultFacetKey) != null)
{
string tmp = entity.GetFacet<Subscription>(Subscription.DefaultFacetKey).SubscriptionDate;
if (!string.IsNullOrEmpty(tmp) && DateTime.TryParse(tmp, out DateTime outDate))
model1.SubscriptionDate = outDate.ToString(DateFormatConstant.DisplayDateFormat);
else
model1.SubscriptionDate = tmp;
}
// ContactIdentifier identifier = entity.Identifiers.FirstOrDefault(x => x.Source.Equals("ListManager", StringComparison.OrdinalIgnoreCase)) ?? entity.Identifiers.FirstOrDefault();
ContactIdentifier identifier = entity.Identifiers.FirstOrDefault(x => x.Source.Equals("ListManager", StringComparison.OrdinalIgnoreCase) || x.Source.Equals("form", StringComparison.OrdinalIgnoreCase)) ?? entity.Identifiers.FirstOrDefault();
model1.Identifier = identifier?.Identifier;
model1.IdentifierSource = identifier?.Source;
return model1;
}
public ContactSearchResult GetFilteredContacts(ContactList contactList, string searchFilter, int pageIndex, int pageSize)
{
IContactSource contactSource = _contactSourceFactory.GetSource(contactList.ContactListDefinition);
FilteringSource source2 = new FilteringSource(contactSource, searchFilter);
string[] facets = new string[] { "ListSubscriptions", "Personal", "Emails", "Subscriptions" };
return new ContactSearchResult(_segmentationService.GetContacts(source2, pageSize * pageIndex, pageSize, facets), (int)_segmentationService.GetCount(contactSource));
}
public void Unsubscribe(Guid listId, Guid contactId)
{
using (new Sitecore.SecurityModel.SecurityDisabler())
{
ContactList contactList = EnsureList(listId);
EnsureContactList(contactList.ContactListDefinition.Type);
_subscriptionService.Unsubscribe(listId, contactId);
}
}
private static void EnsureContactList(ListType listType)
{
if (listType != 0)
throw new UnprocessableEntityException(Translate.Text("Operation is not supported for segmented lists."));
}
public void CreateSubscribedContact(Guid listId, SubscriptionContactDataModel contactDataModel)
{
Assert.ArgumentNotNull(contactDataModel, "contactDataModel");
try
{
using (new SecurityDisabler())
{
ContactList contactList = EnsureList(listId);
EnsureContactList(contactList.ContactListDefinition.Type);
string identifierSource;
if (contactDataModel.IdentifierSource == null)
identifierSource = "ListManager";
else
{
Log.Info($"IsMatch: {SourceValidator.IsMatch(contactDataModel.IdentifierSource)}", this);
if (!SourceValidator.IsMatch(contactDataModel.IdentifierSource))
return;
identifierSource = contactDataModel.IdentifierSource;
}
if (contactList.ContactListDefinition.AddSubscriptionDefinitionId(SubscriptionItemIds.ManualSubscriptionId))
_contactListProvider.Save(contactList);
SubscriptionContactDataModel contactDataModel2 = new SubscriptionContactDataModel()
{
Identifier = contactDataModel.Identifier,
IdentifierSource = identifierSource,
FirstName = contactDataModel.FirstName,
LastName = contactDataModel.LastName,
Email = contactDataModel.Email,
SubscriptionDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
using (new SecurityDisabler())
{
using (XConnectClient client = SitecoreXConnectClientConfiguration.GetClient())
{
IdentifiedContactReference existIdentifier = new IdentifiedContactReference(contactDataModel2.IdentifierSource, contactDataModel2.Identifier.ToLower());
Contact knownContact = client.Get(existIdentifier, new ContactExecutionOptions(new ContactExpandOptions(ListSubscriptions.DefaultFacetKey, PersonalInformation.DefaultFacetKey, EmailAddressList.DefaultFacetKey, Subscription.DefaultFacetKey)));
if (knownContact == null)
{
//create contact by email identidier
ContactIdentifier mailIdentifier = new ContactIdentifier(contactDataModel2.IdentifierSource, contactDataModel2.Email, ContactIdentifierType.Known);
ContactIdentifier[] identifiers = new ContactIdentifier[] { mailIdentifier };
knownContact = new Contact(identifiers);
//create email facet to allow list show email
EmailAddressList emailFacet = new EmailAddressList(new EmailAddress(contactDataModel2.Email, false), "Preferred");
client.SetFacet(knownContact, EmailAddressList.DefaultFacetKey, emailFacet);
Subscription subscriptionFacet = knownContact.GetFacet<Subscription>(Subscription.DefaultFacetKey) ?? new Subscription();
subscriptionFacet.SubscriptionDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
client.SetFacet(knownContact, Subscription.DefaultFacetKey, subscriptionFacet);
ContactListSubscription subscription = new ContactListSubscription(DateTime.UtcNow, true, listId);
List<ContactListSubscription> subscriptionsList = new List<ContactListSubscription>
{
subscription
};
ListSubscriptions listFacet = new ListSubscriptions() { Subscriptions = subscriptionsList };
client.SetFacet(knownContact, ListSubscriptions.DefaultFacetKey, listFacet);
PersonalInformation pi = new PersonalInformation
{
FirstName = contactDataModel2.FirstName,
LastName = contactDataModel2.LastName
};
knownContact = client.Get(existIdentifier, new ContactExecutionOptions(new ContactExpandOptions(ListSubscriptions.DefaultFacetKey, PersonalInformation.DefaultFacetKey, EmailAddressList.DefaultFacetKey, Subscription.DefaultFacetKey)));
client.SetFacet(knownContact, PersonalInformation.DefaultFacetKey, pi);
client.AddContact(knownContact);
}
else
{
ContactListSubscription subscription = new ContactListSubscription(DateTime.UtcNow, true, listId);
List<ContactListSubscription> subscribeList = knownContact.ListSubscriptions()?.Subscriptions?.ToList();
if (subscribeList == null)
{
List<ContactListSubscription> subscriptionsList = new List<ContactListSubscription>
{
subscription
};
ListSubscriptions listFacet = new ListSubscriptions() { Subscriptions = subscriptionsList };
client.SetFacet(knownContact, ListSubscriptions.DefaultFacetKey, listFacet);
Subscription subscriptDateFacet = knownContact.GetFacet<Subscription>(Subscription.DefaultFacetKey) ?? new Subscription();
subscriptDateFacet.SubscriptionDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
client.SetFacet(knownContact, Subscription.DefaultFacetKey, subscriptDateFacet);
PersonalInformation pi = knownContact.GetFacet<PersonalInformation>(PersonalInformation.DefaultFacetKey) ?? new PersonalInformation();
pi.FirstName = contactDataModel2.FirstName;
pi.LastName = contactDataModel2.LastName;
client.SetFacet(knownContact, PersonalInformation.DefaultFacetKey, pi);
}
else
{
ListSubscriptions contactSubscriptions = knownContact.ListSubscriptions();
if (!contactSubscriptions.Subscriptions.Any(x => x.ListDefinitionId.Equals(listId)))
{
contactSubscriptions.Subscriptions.Add(subscription);
client.SetListSubscriptions(knownContact, contactSubscriptions);
}
Subscription subscriptDateFacet = knownContact.GetFacet<Subscription>(Subscription.DefaultFacetKey) ?? new Subscription();
subscriptDateFacet.SubscriptionDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
client.SetFacet(knownContact, Subscription.DefaultFacetKey, subscriptDateFacet);
PersonalInformation pi = knownContact.GetFacet<PersonalInformation>(PersonalInformation.DefaultFacetKey) ?? new PersonalInformation();
pi.FirstName = contactDataModel2.FirstName;
pi.LastName = contactDataModel2.LastName;
client.SetFacet(knownContact, PersonalInformation.DefaultFacetKey, pi);
}
}
client.Submit();
}
}
}
}
catch (Exception ex)
{
Log.Error(ex.ToString(), ex, this);
}
}
}
public class CustomContactProvider : IContactProvider
{
private readonly IContactSourceFactory _contactSourceFactory;
private readonly ISegmentationService _segmentationService;
public CustomContactProvider(ISegmentationService segmentationService, IContactSourceFactory contactSourceFactory)
{
Assert.ArgumentNotNull(segmentationService, "segmentationService");
Assert.ArgumentNotNull(contactSourceFactory, "contactSourceFactory");
_segmentationService = segmentationService;
_contactSourceFactory = contactSourceFactory;
}
public ContactSearchResult GetFilteredContacts(ContactList contactList, string searchFilter, int pageIndex, int pageSize)
{
IContactSource source = _contactSourceFactory.GetSource(contactList.ContactListDefinition);
FilteringSource contactSource = new FilteringSource(source, searchFilter);
IEnumerable<Contact> contacts = _segmentationService.GetContacts(contactSource, pageSize * pageIndex, pageSize, "ListSubscriptions", "Personal", "Emails", "Subscriptions");
long count = _segmentationService.GetCount(source);
return new ContactSearchResult(contacts, (int)count);
}
public IEntityBatchEnumerator<Contact> GetContactBatchEnumerator(ContactList contactList, int batchSize, params string[] facets)
{
IContactSource source = _contactSourceFactory.GetSource(contactList.ContactListDefinition);
return _segmentationService.GetContactBatchEnumerator(source, batchSize, facets);
}
public long GetCount(ContactList contactList)
{
IContactSource source = _contactSourceFactory.GetSource(contactList.ContactListDefinition);
return _segmentationService.GetCount(source);
}
}
public class SubscriptionContactDataModel : Sitecore.ListManagement.Services.Model.ContactDataModel
{
public string SubscriptionDate { get; set; }
}
新建自己的 api
[System.Web.Http.RoutePrefix("sitecore/api/customlists/{listId}/contacts")]
public class CustomContactsController : ServicesApiController
{
private readonly ICustomListSubscriptionsStore<SubscriptionContactDataModel> _listSubscriptionsStore;
private readonly BaseLog _log;
public CustomContactsController()
{
IContactListProvider clp = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IContactListProvider>();
var sp = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<ISubscriptionService>();
var csf = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IContactSourceFactory>();
var sss = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<ISegmentationService>();
var cp = new CustomContactProvider(sss, csf);
var or = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<IListOperationRepository>();
var lss = Sitecore.DependencyInjection.ServiceLocator.ServiceProvider.GetService<ICustomListSubscriptionsStore<SubscriptionContactDataModel>>();
int batchSize = Sitecore.Configuration.Settings.GetIntSetting("ListManagement.BatchSize", 250);
_listSubscriptionsStore = new CustomListSubscriptionsStore(clp, sp, cp, or, batchSize, csf, sss);
}
[System.Web.Http.Route]
[System.Web.Http.HttpGet]
[System.Web.Http.ActionName("customaction")]
public virtual IEnumerable<SubscriptionContactDataModel> GetEntities(Guid listId, string filter = " ", int pageIndex = 0, int pageSize = 20)
{
return _listSubscriptionsStore.GetSubscribers(listId, filter, pageIndex, pageSize);
}
}
public class RegisterHttpRoutes
{
public void Process(PipelineArgs args)
{
GlobalConfiguration.Configure(Configure);
}
protected void Configure(HttpConfiguration configuration)
{
var routes = configuration.Routes;
routes.MapHttpRoute("CustomContacts", "sitecore/api/customlists/{listId}/contacts", new
{
controller = "CustomContacts",
action = "customaction",
});
}
}
新建 Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
<sitecore>
<xconnect>
<runtime type="Sitecore.XConnect.Client.Configuration.RuntimeModelConfiguration,Sitecore.XConnect.Client.Configuration">
<schemas hint="list:AddModelConfiguration">
<schema name="CustomCollectionModel" type="Sitecore.XConnect.Client.Configuration.StaticModelConfiguration,Sitecore.XConnect.Client.Configuration" patch:after="schema[@name='collectionmodel']">
<param desc="modeltype">LinkReit.Feature.XConnect.CustomFacets.Models.SubscriptionCollectionModel, LinkReit.Feature.XConnect.CustomFacets</param>
</schema>
</schemas>
</runtime>
</xconnect>
<pipelines>
<initialize>
<processor type="LinkReit.Feature.XConnect.CustomFacets.Models.RegisterHttpRoutes, LinkReit.Feature.XConnect.CustomFacets"/>
</initialize>
</pipelines>
</sitecore>
</configuration>
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore role:require="Standalone or ContentManagement">
<import>
<facetMapper type="Sitecore.ListManagement.XConnect.Web.Import.CompositeFacetMapperCollection, Sitecore.ListManagement.XConnect.Web">
<param resolve="true" type="Sitecore.Abstractions.BaseLog, Sitecore.Kernel"/>
<facetMappers hint="list:Add">
<facetMapper type="LinkReit.Feature.XConnect.CustomFacets.Mappers.SubscriptionFacetMapper, LinkReit.Feature.XConnect.CustomFacets" />
</facetMappers>
</facetMapper>
</import>
<settings>
<setting name="ListManagement.Import.FacetsToMap" value="Emails|Personal|Addresses|Subscriptions" />
</settings>
<listManager>
<export>
<field name="Subscriptions" type="LinkReit.Feature.XConnect.CustomFacets.Mappers.SubscriptionContactDataReader, LinkReit.Feature.XConnect.CustomFacets" />
</export>
</listManager>
</sitecore>
</configuration>
新建 xml 配置(名字以 sc 開頭,xml 結尾)
<Settings>
<Sitecore>
<XConnect>
<Services>
<XConnect.Client.Configuration>
<Options>
<Models>
<CustomCollectionModel>
<TypeName>LinkReit.Feature.XConnect.CustomFacets.Models.SubscriptionCollectionModel, LinkReit.Feature.XConnect.CustomFacets</TypeName>
</CustomCollectionModel>
</Models>
</Options>
</XConnect.Client.Configuration>
</Services>
</XConnect>
</Sitecore>
</Settings>
<Settings>
<!--
Marketing Automation contact loader configuration
-->
<Sitecore>
<XConnect>
<MarketingAutomation>
<Engine>
<Services>
<MarketingAutomation.Loading.ContactFacetsConfigurator>
<Options>
<IncludeFacetNames>
<Subscriptions>Subscriptions</Subscriptions>
</IncludeFacetNames>
</Options>
</MarketingAutomation.Loading.ContactFacetsConfigurator>
</Services>
</Engine>
</MarketingAutomation>
</XConnect>
</Sitecore>
</Settings>