Skip to content

[DataSubjectId] ​

The [DataSubjectId] attribute identifies the property that holds the data subject identifier - the person whose personal data this entity contains. Tayra uses this value to derive the encryption key ID in the key store.

Basic Usage ​

Apply [DataSubjectId] to a Guid or string property:

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

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

The encryption key for this entity will be stored under the key ID equal to Id.ToString() (e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890").

Key ID Prefix ​

The Prefix property prepends a string to the key ID, which is useful for namespacing keys by entity type:

cs
public class PrefixedCustomer
{
    [DataSubjectId(Prefix = "cust-")]
    public Guid CustomerId { get; set; }

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

With Prefix = "cust-", the key ID becomes "cust-a1b2c3d4-e5f6-7890-abcd-ef1234567890". This prevents key ID collisions when different entity types share the same subject identifier.

Prefix Conventions

Use short, descriptive prefixes with a trailing delimiter:

  • "cust-" for customers
  • "patient-" for patients
  • "emp-" for employees

The prefix is included in the key store, so it affects crypto-shredding: DeleteKeyAsync("cust-{id}") only deletes the customer key, not keys for the same person in other contexts.

Multiple Groups ​

A single entity can have multiple [DataSubjectId] attributes for different groups. Each group uses its own encryption key:

cs
public class InsuranceClaim
{
    [DataSubjectId(Group = "claimant")]
    public Guid ClaimantId { get; set; }

    [DataSubjectId(Group = "witness")]
    public Guid WitnessId { get; set; }

    [PersonalData(Group = "claimant")]
    public string ClaimantName { get; set; } = "";

    [PersonalData(Group = "witness")]
    public string WitnessName { get; set; } = "";
}
anchor

In this example:

  • ClaimantName is encrypted with the key derived from ClaimantId (group "claimant").
  • WitnessName is encrypted with the key derived from WitnessId (group "witness").
  • Shredding the claimant's key does not affect the witness's data, and vice versa.

Properties Reference ​

PropertyTypeDefaultDescription
Groupstring?nullGroup name for multi-key scenarios. Only [PersonalData] fields in the same group use this subject's key.
Prefixstring?nullPrepended to the key ID in the key store. Useful for namespacing by entity type.
AllowMissingboolfalseWhen true, a record whose subject id is null is stored with this group's personal data unencrypted instead of failing. See Records without a subject yet.

Records without a subject yet ​

Some records exist before their data subject is known: a legacy row whose patient identity is backfilled later, for example. Inventing a placeholder subject is the wrong fix, because it gives the person a second identity, and erasing the real one would then shred only part of their data.

For that case, opt the subject in with AllowMissing:

cs
public class ProvidedCareLifecycle
{
    [DataSubjectId(AllowMissing = true)]
    public string? SubjectId { get; set; }   // null until backfilled

    [PersonalData]
    public string StreetName { get; set; } = "";
}
anchor
  • Subject null: the record is stored as it is. This group's personal data is not encrypted, and each such write raises a StoredWithoutSubject audit event and increments the tayra.encrypt.subject_missing metric, so the cleartext records stay findable.
  • Subject set: encrypted as usual. Once the backfill sets the subject, the next write (or a projection rebuild) encrypts the record.
  • Reading a record without a subject returns it as stored, whether or not AllowMissing is set. A missing subject is never treated as a shredded key.
  • The opt-in is per subject: other types, and other groups on the same type, still fail closed. The PII inventory reports which subjects allow it.

Cleartext until backfilled

Records stored this way hold personal data in the clear, outside crypto-shredding, until they get a subject. Use AllowMissing only where a subject is genuinely unknowable at write time, and track the backfill through the audit event or metric.

Key ID Derivation ​

The encryption key ID is built from three components:

{Prefix}{SubjectId.ToString()}{:Group}
PrefixSubject IDGroupResulting Key ID
nullabc-123nullabc-123
"cust-"abc-123nullcust-abc-123
nullabc-123"medical"abc-123:medical
"cust-"abc-123"medical"cust-abc-123:medical

Supported Types ​

[DataSubjectId] can be applied to:

TypeNotes
GuidRecommended. Globally unique, no collisions.
stringUseful when your identifier is a natural key (e.g., email, external ID).
int, long, other typesWork: the key id is the value's ToString(). Make sure that text is stable and unique per subject.

The subject id is turned into the key id with ToString(), so any type works as long as its text form identifies exactly one subject. A default value (Guid.Empty, an empty or whitespace string, 0) is rejected, because every record without a real id would otherwise share one key. The analyzer rule TAYRA002 does not check the type: it flags a [DataSubjectId] on a type with no [PersonalData] members.

Null Subject IDs Fail Closed

If the [DataSubjectId] property value is null at encryption time, EncryptAsync throws an InvalidOperationException. Without a subject identifier Tayra cannot resolve an encryption key, and continuing would persist the group's fields as plaintext. Ensure subject IDs are always populated before calling EncryptAsync, or, where a record genuinely has no subject yet, opt in with AllowMissing. (Earlier releases logged a warning and skipped the group, silently leaving plaintext behind.)

Decryption does not fail on a null subject either, and does not treat it as shredded. Since encryption never writes ciphertext without a subject, DecryptAsync returns that group's fields exactly as stored: a record saved in the clear before its subject was known reads back unchanged. Only a key that is actually gone (crypto-shredded) follows the replacement-value path.

[DataSubjectId] (like all Tayra attributes) can be applied to a property or a public instance field; private and static members are not scanned. The subject identifier is read, never written back, so it does not need a setter.

See Also ​