新建 facet -- sitecore

发布时间 2023-04-23 11:31:33作者: 灵火

官方文檔:https://doc.sitecore.com/xp/en/developers/100/sitecore-experience-platform/deploy-a-custom-model.html

博客三部曲:

  1. https://sitecorewithraman.wordpress.com/2020/08/02/sitecore-xconnect-custom-facets-part-i/
  2. https://sitecorewithraman.wordpress.com/2021/03/27/customize-list-manager-part-ii/
  3. 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>