Identity collectors - Gather objects associated with a visitor

Data protection laws such as the European Union’s GDPR often specify that visitors to a site must be able to see their personal data that has been collected, and they can request that it be removed. Identity collection is a key part of this process.

Let’s create an identity collector for gathering visitor identities, such as contacts, members, and customers.

You can read more about GDPR compliance in the Xperience by Kentico Documentation.

Before you start

This guide requires the following:

The examples in this guide require that you:

The example presented in this Data protection guide series is a functional implementation of data protection for contacts, members, and customers in Xperience by Kentico. Note that it does not cover the collection and erasure of Customer data platform profiles and their associated data.

You can copy-paste the code samples into your own solution.

However, if you choose to do so, make sure to consult your legal team to determine whether the implementation, texts, and consent levels meet the requirements of your region and market.

Code samples

You can find a project with completed, working versions of code samples from this guide and others in the finished branch of the Training guides repository.

The main branch of the repository provides a starting point to code along with the guides.

The code samples in this guide are for .NET 8 only.

They come from a project that uses implicit using directives. You may need to add additional using directives to your code if your project does not use this feature.

Understand the components

In the Configuration → Data Protection application in Xperience by Kentico, you’ll notice tabs for Data portability, Right to access, and Right to be forgotten.

These correspond to aspects of the GDPR but are applicable to other regulations as well, and each is used for dealing with visitors’ personal data.

The Data portability tab, once implemented, allows administrators to gather a visitor’s data in a machine-readable format. In contrast, the Right to access tab collects data in a human-readable format. Finally, the Right to be forgotten tab allows a visitor’s data to be wiped from the site.

However, the code that carries out these actions needs to be implemented, as there is no one-size-fits-all solution to the virtually limitless ways a site could collect and store personal information. The code samples from this series cover just one example site with its own specific requirements.

Identify the first step

Data collection and erasure require an Identity collector as the first step.

Identity collectors are used to gather a collection of Xperience objects representing the visitor, called Identities. For example, real-world people could be represented by a User object, one or more Contact objects, or any custom objects, such as Customer, Author, Contractor, etc.

Once the identity collector gathers these identities, it passes them to other components. These components can use these identities to find objects that reference them, e.g. activities performed by a contact.

We will cover the creation of an identity collector, the first step of this process.

Note: The system uses identifiers, such as an email address, to find Identity objects. It is critical that any data you collect and store always contains one of the following, so that it can be found.

  • a reference to one of the identity objects
  • the identifier itself

Consider an example where you use an email address as the identifier and create a form that collects the first and last name of a visitor but no email address. In such a case, there would be no way to find that information using a supplied email address, and GDPR compliance would be impossible when a visitor exercises their right to be forgotten.

Implement the identity collector

  1. Create a Collectors folder in ~/Features/DataProtection of the TrainingGuides.Web project.

  2. Add an IdentityCollector.cs file to the folder, and add a namespace matching the folder structure (TrainingGuides.Web.Features.DataProtection.Collectors).

  3. Implement the IIdentityCollector interface.

  4. Take info providers for contacts, members, and customers as parameters in the primary constructor.

  5. Implement the Collect(IDictionary<string, object> dataSubjectFilter, List<BaseInfo> identities) method from the interface.

  6. Use the PersonalDataConstants.DATA_SUBJECT_IDENTIFIER_KEY constant to get the data subject email.

  7. Retrieve any contacts, members, and customers that have this email and add them to the identities parameter.

  8. Define additional logic that creates a new contact and a new customer with the supplied email address if the queries find none.

    It is possible for form data containing an email address to exist even if there is no contact or customer in the database with that email.

    These dummy objects allow the Data collector, which will be covered in the next part of this series, to find form data and third-party addresses associated with the provided email address even when no contact or customer exists.

C#
IdentityCollector.cs

using CMS.Commerce;
using CMS.ContactManagement;
using CMS.DataEngine;
using CMS.DataProtection;
using CMS.Membership;

namespace TrainingGuides.Web.Features.DataProtection.Collectors;

public class IdentityCollector(IInfoProvider<ContactInfo> contactInfoProvider, IInfoProvider<MemberInfo> memberInfoProvider, IInfoProvider<CustomerInfo> customerInfoProvider) : IIdentityCollector
{
    public void Collect(IDictionary<string, object> dataSubjectFilter, List<BaseInfo> identities)
    {
        // Does nothing if the identifier input value is not available or empty
        if (!dataSubjectFilter.TryGetValue(PersonalDataConstants.DATA_SUBJECT_IDENTIFIER_KEY, out object? value))
        {
            return;
        }
        string? email = value as string;
        if (string.IsNullOrWhiteSpace(email))
        {
            return;
        }

        var contacts = contactInfoProvider
            .Get()
            .WhereEquals(nameof(ContactInfo.ContactEmail), email)
            .ToList();

        // If no contact exists with the provided email, create a new one.
        // This will allow us to retrieve form submissions that contain the email even if they are not currently tied to a contact.
        if (contacts.Count() == 0)
        {
            contacts.Add(new ContactInfo() { ContactEmail = email });
        }

        identities.AddRange(contacts);

        var members = memberInfoProvider
            .Get()
            .WhereEquals(nameof(MemberInfo.MemberEmail), email)
            .ToList();

        identities.AddRange(members);

        var customers = customerInfoProvider
            .Get()
            .WhereEquals(nameof(CustomerInfo.CustomerEmail), email)
            .ToList();

        // If no customer exists with the provided email, create a new one.
        // This will allow us to retrieve third-party customer and order addresses that contain the email even if they are associated with a different customer.
        if (customers.Count() == 0)
        {
            customers.Add(new CustomerInfo() { CustomerEmail = email });
        }

        identities.AddRange(customers);
    }
}

Register the Identity collector

Now that the Identity collector is in place, we can register it.

  1. Create a new DataProtectionRegistrationModule class in ~/Features/DataProtection/Shared folder of the TrainingGuides.Web project.
  2. Inherit from Xperience’s Module class, and override OnInit to add IdentityCollector to the IdentityCollectorRegister.
  3. Resolve an IServiceProvider and use it to create an instance of the IdentityCollector.
  4. Register the module using an assembly attribute.
C#
DataProtectionRegistrationModule.cs

using CMS;
using CMS.Core;
using CMS.DataEngine;
using CMS.DataProtection;
using TrainingGuides.Web.Features.DataProtection.Collectors;
using TrainingGuides.Web.Features.DataProtection.Shared;

[assembly: RegisterModule(
    type: typeof(DataProtectionRegistrationModule))]

namespace TrainingGuides.Web.Features.DataProtection.Shared;

public class DataProtectionRegistrationModule : Module
{
    public DataProtectionRegistrationModule()
        : base("DataProtectionRegistration")
    {
    }

    // Contains initialization code that is executed when the application starts
    protected override void OnInit(ModuleInitParameters parameters)
    {
        var serviceProvider = parameters.Services.GetRequiredService<IServiceProvider>();

        base.OnInit(parameters);

        // Adds the IdentityCollector to the collection of registered identity collectors
        IdentityCollectorRegister.Instance.Add(ActivatorUtilities.CreateInstance<IdentityCollector>(serviceProvider));
    }
}

Now, Xperience will know how to utilize the IdentityCollector class for data portability, collection, and erasure requests. When you implement a Data collector, covered later on in this series, Xperience will use the identity collector class to gather a list of identity objects and pass them to the data collector to retrieve associated data.

The IdentityCollectorRegister, like the other data protection registers, is a queue that can contain multiple collectors. If the same class is registered to this queue more than once, it will be called multiple times.

What’s next?

The following guide in this series will cover the process of creating a Data collector and some utility classes to help it run smoothly.