Skip to content

[DeepPersonalData]

The [DeepPersonalData] attribute marks a property or public instance field that holds a nested object containing [PersonalData] members. Tayra will recursively process the nested object, encrypting and decrypting its personal data fields using the parent entity's encryption key.

Basic Usage

Apply [DeepPersonalData] to a class-type property. The nested class uses [PersonalData] on its own fields, but does not need its own [DataSubjectId]:

cs
public class Address
{
    [PersonalData]
    public string Street { get; set; } = "";

    [PersonalData]
    public string City { get; set; } = "";

    [PersonalData]
    public string PostalCode { get; set; } = "";
}

public class CustomerWithAddress
{
    [DataSubjectId]
    public Guid Id { get; set; }

    [PersonalData]
    public string Name { get; set; } = "";

    [DeepPersonalData]
    public Address? HomeAddress { get; set; }
}
anchor

In this example:

  • CustomerWithAddress has a [DataSubjectId] on Id and a [PersonalData] field Name.
  • HomeAddress is marked with [DeepPersonalData], so Tayra recurses into the Address object.
  • The Address class has three [PersonalData] fields: Street, City, and PostalCode.
  • All fields - both on the parent and the nested object - are encrypted with the same key derived from Id.

Parent Key Inheritance

When a nested object does not have its own [DataSubjectId], it inherits the encryption key from its parent entity. This means:

  1. The parent entity's [DataSubjectId] is used to look up or create the encryption key.
  2. All [PersonalData] fields in the nested object are encrypted with that same key.
  3. Crypto-shredding the parent's key destroys both the parent's and the nested object's personal data.

If the nested object does have its own [DataSubjectId], it uses its own independent key instead.

When to Use Independent Keys

Give nested objects their own [DataSubjectId] when they represent a different data subject. For example, an Order might have a [DataSubjectId] for the customer, while a nested DeliveryDriver object has its own [DataSubjectId] - shredding the customer's key should not affect the driver's data.

Collections

[DeepPersonalData] also works on collections of objects. If the property type is a List<T>, T[], or any type implementing IList<T>, Tayra iterates through each element and processes it:

csharp
public class CustomerWithAddresses
{
    [DataSubjectId]
    public Guid Id { get; set; }

    [DeepPersonalData]
    public List<Address>? Addresses { get; set; }
}

Each Address in the list will have its [PersonalData] fields encrypted using the parent's key.

Cycles and Shared References

Object graphs with cycles (e.g., a child object holding a reference back to its parent) or shared "diamond" references (the same object reachable through two different [DeepPersonalData] paths) are handled safely. Tayra tracks visited objects by reference during each EncryptAsync/DecryptAsync call:

  • Cycles terminate cleanly instead of overflowing the stack.
  • Shared references are processed exactly once, so a shared object is never double-encrypted (which would corrupt its data).

No configuration is needed - the tracking only kicks in for graphs that contain deep fields, so flat entities pay no overhead.

Properties or Public Instance Fields

[DeepPersonalData] can be applied to a property or a public instance field; private and static members are not scanned. The nested object is mutated in place, so the member does not need a setter - a get-only property or readonly field is fine here, since Tayra never reassigns the reference.

Scope Property

The [DeepPersonalData] attribute has an optional Scope property:

PropertyTypeDefaultDescription
Scopestring?nullAn optional scope identifier for the nested data. Reserved for future use in scoped metadata resolution.

Null Handling

If the [DeepPersonalData] property is null, Tayra skips it without error. This is safe:

csharp
var customer = new CustomerWithAddress
{
    Id = Guid.NewGuid(),
    Name = "Jane",
    HomeAddress = null, // Skipped during encryption
};

await fieldEncrypter.EncryptAsync(customer); // Only Name is encrypted

See Also