Skip to content

PII Attribute

The [PII] attribute marks data as personally identifiable information (PII) under GDPR. When Chronicle sees this attribute on an event property or a type, it encrypts the value automatically when the event is written to the event log and decrypts it transparently on read.

Personal data rarely sits in a flat list of properties. A date of birth arrives wrapped in a VerifiedDateOfBirth value object alongside who verified it; a diagnosis carries the condition and the clinician together. Chronicle follows the marker down through that structure: whether you mark a concept nested inside a value object, or the value object type itself, encryption lands on the individual values at the bottom. The document keeps its shape, each value stays independently encrypted, and the release on read mirrors the encryption on write.

using Cratis.Chronicle.Compliance.GDPR;

The signature, for reference:

// C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class PIIAttribute(string details = "") : Attribute
// TypeScript
export function pii(details?: string): PropertyDecorator & ClassDecorator

The optional details parameter lets you record why the value is classified as PII — for example, the legal basis under which it is collected or the retention period. This information is stored in the event schema and can be used by compliance reporting tools.

using Cratis.Chronicle.Compliance.GDPR;
[PII("Collected under GDPR Art. 6(1)(b) — necessary for contract performance")]
public record PiiAttrPersonName(string Value) : ConceptAs<string>(Value);
TargetSupportedNotes
ConceptAs<T> classYesPreferred approach — marks the concept type itself as PII
Event propertyYesMarks a single property of an event as PII
Value object classYesMarks every value the type holds as PII, however deeply nested
Property typed as a value objectYesSame effect, scoped to that one property
Collection propertyYesThe collection is encrypted as a whole — see Collections are encrypted as a whole
EventSourceId or EventSourceId<T>NoThrows PIINotSupportedOnEventSourceId at runtime

You can mark a single property on an event record as PII. This is useful when the property type is a primitive and you cannot or do not want to introduce a dedicated concept type.

using Cratis.Chronicle.Compliance.GDPR;
using Cratis.Chronicle.Events;
[EventType]
public record PiiAttrEmployeeRegistered(
[PII] string FirstName,
[PII] string LastName,
string Department);

When this event is written, FirstName and LastName are encrypted. Department is stored as plaintext.

The preferred approach is to mark the ConceptAs<T> type itself as PII. Every property across every event that uses this type is then automatically encrypted — you declare the rule once and it applies everywhere.

using Cratis.Chronicle.Compliance.GDPR;
[PII]
public record PiiAttrConceptPersonName(string Value) : ConceptAs<string>(Value)
{
public static readonly PiiAttrConceptPersonName NotSet = new(string.Empty);
public static implicit operator string(PiiAttrConceptPersonName name) => name.Value;
public static implicit operator PiiAttrConceptPersonName(string value) => new(value);
}

Not every piece of personal data is a single value. When a whole value object is personal — a diagnosis, a passport, a home address — mark the type itself. Every value it holds is then treated as PII wherever that type appears, without annotating each member.

using Cratis.Chronicle.Compliance.GDPR;
// Every value this type holds is personal, so mark the type once.
[PII]
public record PiiAttrDiagnosis(string Condition, string DiagnosedBy);
// Both Condition and DiagnosedBy are encrypted wherever a PiiAttrDiagnosis appears.
public record PiiAttrPatientRecord(string Name, PiiAttrDiagnosis Diagnosis);

Chronicle pushes the marker down to the individual values rather than encrypting the object as one blob. In storage you still see a Diagnosis sub-document with a Condition and a DiagnosedBy field; both hold ciphertext. That matters for more than tidiness: the shape is what lets the read model materialize back into its value-object type, and it keeps each value separately encrypted rather than fused into a single opaque string.

The same happens when you mark a property whose type is a value object — the effect is identical, just scoped to that one property instead of to the type everywhere.

The marker does not have to sit at the top level. A ConceptAs<T> marked [PII] is found wherever it ends up — directly on an event, one level down inside a value object, or deeper still.

using Cratis.Chronicle.Compliance.GDPR;
[PII]
public record PiiAttrDateOfBirth(string Value) : ConceptAs<string>(Value)
{
public static implicit operator PiiAttrDateOfBirth(string value) => new(value);
}
// The concept sits one level down, inside a value object.
public record PiiAttrVerifiedDateOfBirth(PiiAttrDateOfBirth DateOfBirth, string VerifiedBy);
// Chronicle still finds it: dateOfBirth.dateOfBirth is encrypted, verifiedBy is not.
public record PiiAttrExpressVerification(string Name, PiiAttrVerifiedDateOfBirth DateOfBirth);

Here only dateOfBirth.dateOfBirth is encrypted. verifiedBy sits beside it in the same value object and stays readable, because the concept carries the classification, not its container.

This is what makes concept-level [PII] worth reaching for: you declare the rule once on the type, and it holds no matter how the value is later composed into events and read models.

Applying [PII] to a type that inherits from EventSourceId or EventSourceId<T> throws PIINotSupportedOnEventSourceId at runtime:

using Cratis.Chronicle.Compliance.GDPR;
using Cratis.Chronicle.Events;
// ❌ This will throw PIINotSupportedOnEventSourceId
[PII]
public record PiiAttrEmployeeId(Guid Value) : EventSourceId<Guid>(Value);

Event source identifiers are used to look up encryption keys and group events. Encrypting them would make key lookup impossible. If the identifier itself is sensitive, use a non-sensitive surrogate (such as a random Guid) as the event source identifier and store the sensitive value in a [PII]-marked event property.

A [PII] marker on a collection property behaves differently from one on a value object. The collection is encrypted as a single value rather than element by element, and its shape is restored when it is released. That keeps the data protected, but the individual elements are not separately encrypted and cannot be queried or indexed on the server.

When you need per-element encryption, model the collection as its own person-scoped read model keyed by the subject and join at the query edge, rather than nesting it inside a larger document.

The details parameter is a free-text description stored in the event schema. It is never used for encryption — it exists solely to record why a value is classified as PII for compliance documentation and auditing purposes.

using Cratis.Chronicle.Compliance.GDPR;
[PII("Full legal name — required for contract identification")]
public record PiiAttrLegalName(string Value) : ConceptAs<string>(Value);