Collection Encryption
Tayra supports encrypting collections of personal data. When a property marked with [PersonalData] is a List<string>, string[], or other supported collection type, Tayra encrypts each element individually. This is useful for entities that store multiple PII values in a single field, such as phone numbers, email aliases, medical allergies, or previous addresses.
Defining Collection PII
Mark collection properties with the [PersonalData] attribute, the same way you mark scalar string properties. Tayra detects the collection type automatically and encrypts each element:
public class PatientRecord
{
[DataSubjectId(Prefix = "patient-")]
public Guid PatientId { get; set; }
[PersonalData]
public string FullName { get; set; } = "";
[PersonalData]
public List<string> Allergies { get; set; } = new();
[PersonalData]
public string[] PreviousSurnames { get; set; } = Array.Empty<string>();
public string Department { get; set; } = "";
}In the example above:
Allergiesis aList<string>- each allergy is encrypted individuallyPreviousSurnamesis astring[]- each surname is encrypted individuallyFullNameis a regularstring- encrypted as usualDepartmenthas no attribute - left untouched
Per-Element Encryption
Each element in the collection is encrypted independently with the same key (derived from the [DataSubjectId]). This means:
- Individual elements can be added or removed without re-encrypting the entire collection
- Each element has its own nonce (initialization vector), so identical plaintext values produce different ciphertext
- The collection size is preserved - a list with 3 elements before encryption still has 3 elements after
// Before encryption
patient.Allergies = new List<string> { "Penicillin", "Peanuts", "Latex" };
// After encryption - 3 Base64-encoded ciphertext strings
patient.Allergies = new List<string>
{
"AQzR4x5k...", // Encrypted "Penicillin"
"AQnT7y2m...", // Encrypted "Peanuts"
"AQ8pL9dw...", // Encrypted "Latex"
};Supported Collection Types
Any property whose type implements IEnumerable<string> is detected as a string collection. At runtime, the collection instance must be mutable:
| Type | Encryption | Decryption | Notes |
|---|---|---|---|
List<string> | In-place mutation | In-place mutation | Elements are replaced with encrypted/decrypted values |
string[] | In-place mutation | In-place mutation | Array elements are replaced directly |
IList<string> | In-place mutation | In-place mutation | Works via the IList<string> indexer |
HashSet<string> | Snapshot, clear, re-add | Snapshot, clear, re-add | No indexer; elements are snapshotted, the set is cleared, and processed values are re-added |
Other mutable ICollection<string> | Snapshot, clear, re-add | Snapshot, clear, re-add | Same approach as HashSet<string> |
A property may also be declared as a read-only interface (e.g., IReadOnlyList<string> or IEnumerable<string>) as long as the runtime instance is mutable - writability is checked when the collection is first encrypted or decrypted.
A collection can be declared as a property or as a public instance field. Because elements are mutated in place, the member itself does not need a setter (a readonly field holding a mutable collection is fine); only the runtime collection instance must be mutable.
Crypto-Shredding with Collections
When the encryption key is deleted (crypto-shredding), collection elements follow the same rules as scalar fields:
- If partial redaction is configured, each element's embedded redacted value is returned
- Otherwise, each element is replaced with an empty string (
"")
// After crypto-shredding (key deleted)
patient.Allergies = new List<string> { "", "", "" };Partial Redaction
Collection elements support the same partial redaction modes as scalar fields. Configure redaction on the [PersonalData] attribute:
public class ContactInfo
{
[DataSubjectId]
public Guid SubjectId { get; set; }
[PersonalData(Masking = MaskingStrategies.MaskAfter, MaskingParameter = 3)]
public List<string> EmailAddresses { get; set; } = new();
}After crypto-shredding, each email would retain its first 3 characters:
"jan*****" // was "jane@example.com"
"bob*****" // was "bob@work.org"Limitations
- Only
stringelement types are supported for collection encryption. Collections of non-string types (e.g.,List<int>,List<DateTime>) must use[SerializedPersonalData]on the property with a companionbyte[]field instead. - Null elements in a collection are skipped during encryption and decryption. They remain
nullin the collection. - Read-only collection instances (e.g.,
ImmutableList<string>, aReadOnlyCollection<string>wrapper) cannot be updated and throw anInvalidOperationExceptionat encrypt/decrypt time. (Earlier releases silently skipped them, leaving plaintext behind.) Use a mutable collection such asList<string>,string[], orHashSet<string>. - Deep collections (collections of objects with
[DeepPersonalData]) are supported - Tayra will recursively encrypt PII fields on each element. The element objects must have their own[DataSubjectId]or inherit the parent's key context.
Performance
Collection encryption processes elements sequentially. For very large collections (thousands of elements), the encryption time scales linearly. If performance is a concern, consider batching large collections or encrypting them as a single serialized blob with [SerializedPersonalData].
See Also
- Getting Started - Core encrypt/decrypt/shred workflow
- Audit Trail - Audit events for collection operations
- Observability - Metrics for encryption operations
