Module: Data protection

10 of 13 Pages

Prepare data writers

Some data protection laws require all of a visitor’s tracked personal data to be gathered and delivered to them upon request. A data collector is necessary to implement such functionality in Xperience by Kentico.

Data collectors find other objects associated with a provided set of identities from the Identity collector. In this example, the identities are contacts, and the data collector will find accounts, consent agreements, form data, activities, and other auxiliary data associated with those contacts.

Create data writers

Based on whether data collection is requested through the Data portability or Right to access tab in the Xperience UI, it will invoke registered personal data collectors with the outputFormat parameter set to a constant string value, either PersonalDataFormat.HUMAN_READABLE or PersonalDataFormat.MACHINE_READABLE.

Data collectors are meant to output a string result.

While it is possible to include logic to print in different formats within the code that collects the data, separating it out into dedicated writer classes keeps the code clearer and less redundant.

Lay the groundwork

Before you define the first writer, create a class that represents a column that should be collected and displayed, as well as an interface that both writers will implement.

  1. In the TrainingGuides.Web/Features/DataProtection/Collectors folder, create a CollectedColumn class.

  2. Add properties to hold the display name and a code name.

  3. Define a constructor that takes both properties as parameters.

    C#
    CollectedColumn.cs
    
    
     namespace TrainingGuides.Web.Features.DataProtection.Collectors;
    
     public class CollectedColumn
     {
         public string Name { get; }
    
         public string DisplayName { get; }
    
         public CollectedColumn(string name, string displayName)
         {
             Name = name;
             DisplayName = displayName;
         }
     }
    
     
  4. Create a Writers subfolder in ~/Features/DataProtection and add an interface that both the human and machine-readable writer classes can implement.

  5. Similar to the example in the documentation, add signatures for void methods that create beginning and ending sections and info objects.

  6. Create an additional method signature for writing values.

  7. Add a method signature for a string method that outputs the result of the writer’s operations.

    C#
    IPersonalDataWriter.cs
    
    
     using CMS.DataEngine;
     using TrainingGuides.Web.Features.DataProtection.Collectors;
    
     namespace TrainingGuides.Web.Features.DataProtection.Writers;
    
     public interface IPersonalDataWriter : IDisposable
     {
         void WriteStartSection(string sectionName, string sectionDisplayName);
         void WriteBaseInfo(BaseInfo baseInfo, List<CollectedColumn> columns, Func<string, object, object>? valueTransformationFunction = null);
         void WriteSectionValue(string sectionName, string sectionDisplayName, string value);
         void WriteEndSection();
         string GetResult();
     }
    
     

Add a human-readable writer

  1. In the same folder, define a HumanReadablePersonalDataWriter that implements the IPersonalDataWriter interface.

  2. Define it similarly to the documentation example, utilizing a private string builder property.

  3. Add a method called WriteSectionValue that prints a key and value, ignoring the section name, which the machine-readable counterpart can utilize.

  4. Leave the Dispose method empty.

    You don’t need the method here, but it is important for the machine-readable data writer covered in the next step.

C#
HumanReadablePersonalDataWriter.cs


using CMS.Base;
using CMS.DataEngine;
using System.Globalization;
using System.Text;
using TrainingGuides.Web.Features.DataProtection.Collectors;

namespace TrainingGuides.Web.Features.DataProtection.Writers;

public class HumanReadablePersonalDataWriter : IPersonalDataWriter
{
    private readonly StringBuilder stringBuilder;
    private int indentationLevel;
    private bool ignoreNewLine;

    private static readonly string decimalPrecision = new('#', 26);
    private static readonly string decimalFormat = "{0:0.00" + decimalPrecision + "}";

    public CultureInfo Culture { get; set; } = new CultureInfo(SystemContext.SYSTEM_CULTURE_NAME);

    public HumanReadablePersonalDataWriter()
    {
        stringBuilder = new StringBuilder();
        indentationLevel = 0;
        ignoreNewLine = false;
    }

    public void WriteStartSection(string sectionName, string sectionDisplayName)
    {
        ignoreNewLine = false;
        Indent();

        stringBuilder.AppendLine(sectionDisplayName + ": ");
        indentationLevel++;
    }

    private void Indent() => stringBuilder.Append('\t', indentationLevel);

    public void WriteBaseInfo(BaseInfo baseInfo, List<CollectedColumn> columns,
        Func<string, object, object>? valueTransformationFunction = null)
    {
        if (baseInfo == null)
        {
            throw new ArgumentNullException(nameof(baseInfo));
        }

        foreach (var column in columns)
        {
            string columnName = column.Name;
            string columnDisplayName = column.DisplayName;

            if (string.IsNullOrWhiteSpace(columnDisplayName) ||
                columnName.Equals(baseInfo.TypeInfo.IDColumn, StringComparison.Ordinal) ||
                columnName.Equals(baseInfo.TypeInfo.GUIDColumn, StringComparison.Ordinal))
            {
                continue;
            }

            object value = baseInfo.GetValue(columnName);

            if (value == null)
            {
                continue;
            }

            if (valueTransformationFunction != null)
            {
                value = valueTransformationFunction(columnName, value);
            }

            WriteKeyValue(columnDisplayName, value);
        }
    }

    public void WriteSectionValue(string sectionName, string sectionDisplayName, string value)
    {
        Indent();

        stringBuilder.Append($"{sectionDisplayName}: {value}");
        stringBuilder.AppendLine();
    }

    private void WriteKeyValue(string keyName, object value)
    {
        Indent();
        stringBuilder.Append($"{keyName}: ");

        string format = "{0}";

        if (value is decimal)
        {
            format = decimalFormat;
        }

        stringBuilder.AppendFormat(Culture, format, value);
        stringBuilder.AppendLine();

        ignoreNewLine = true;
    }

    public void WriteEndSection()
    {
        indentationLevel--;

        if (ignoreNewLine)
            return;

        Indent();
        stringBuilder.AppendLine();
        ignoreNewLine = true;
    }

    public string GetResult() => stringBuilder.ToString();

    public void Dispose()
    {
    }
}

Create a machine-readable writer

Add an XML writer that closely mirrors the example in the documentation.

In this case, WriteSectionValue needs to use the sectionName property for the XML tags rather than the display name.

C#
XmlPersonalDataWriter.cs


using CMS.DataEngine;
using CMS.Helpers;
using System.Text;
using System.Xml;
using TrainingGuides.Web.Features.DataProtection.Collectors;

namespace TrainingGuides.Web.Features.DataProtection.Writers;

public class XmlPersonalDataWriter : IPersonalDataWriter
{
    private readonly StringBuilder stringBuilder;
    private readonly XmlWriter xmlWriter;

    public XmlPersonalDataWriter()
    {
        stringBuilder = new StringBuilder();
        xmlWriter = XmlWriter.Create(stringBuilder, new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true });
    }

    public void WriteStartSection(string sectionName, string sectionDisplayName) =>
        xmlWriter.WriteStartElement(TransformElementName(sectionName));

    private string TransformElementName(string originalName) => originalName.Replace('.', '_');

    public void WriteBaseInfo(BaseInfo baseInfo, List<CollectedColumn> columns,
        Func<string, object, object>? valueTransformationFunction = null)
    {
        if (baseInfo == null)
        {
            throw new ArgumentNullException(nameof(baseInfo));
        }

        foreach (var columnTuple in columns)
        {
            string columnName = columnTuple.Name;

            if (string.IsNullOrWhiteSpace(columnTuple.DisplayName))
            {
                continue;
            }

            object value = baseInfo.GetValue(columnName);
            if (value == null)
            {
                continue;
            }

            if (valueTransformationFunction != null)
            {
                value = valueTransformationFunction(columnName, value);
            }

            xmlWriter.WriteStartElement(columnName);
            xmlWriter.WriteValue(XmlHelper.ConvertToString(value, value.GetType()));
            xmlWriter.WriteEndElement();
        }
    }

    public void WriteSectionValue(string sectionName, string sectionDisplayName, string value)
    {
        xmlWriter.WriteStartElement(sectionName);
        xmlWriter.WriteString(value);
        xmlWriter.WriteEndElement();
    }

    public void WriteEndSection() => xmlWriter.WriteEndElement();

    public string GetResult()
    {
        xmlWriter.Flush();

        return stringBuilder.ToString();
    }

    public void Dispose() => xmlWriter.Dispose();
}