Releasing PII
Most of the time you never think about this. GetInstanceById, GetInstances, and GetSnapshotsById on IReadModels already hand you PII decrypted — a projection is decrypted by the kernel before it reaches you, and a reducer-backed instance is decrypted by the client right before it’s returned. See Read models and PII for how that automatic path works and what it takes to mark a property as PII in the first place.
IReadModels.Release is the same decryption, exposed for the cases that skip that path: an instance you built from raw storage, restored from a cache or message payload, or received somewhere that doesn’t release automatically. One built-in case exists today — Watch<TReadModel>() streams changes to you directly and does not call Release first.
using Cratis.Chronicle;using Cratis.Chronicle.Compliance.GDPR;using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
[PII]public record ReleasingPiiRequesterName(string Value) : ConceptAs<string>(Value){ public static readonly ReleasingPiiRequesterName NotSet = new(string.Empty); public static implicit operator string(ReleasingPiiRequesterName name) => name.Value; public static implicit operator ReleasingPiiRequesterName(string value) => new(value);}
[EventType]public record ReleasingPiiSupportTicketOpened(string CustomerId, ReleasingPiiRequesterName RequesterName);
public record ReleasingPiiSupportTicket(string Id, [Subject] string CustomerId, [PII] string RequesterName);
public class ReleasingPiiSupportTicketReducer : IReducerFor<ReleasingPiiSupportTicket>{ public ReleasingPiiSupportTicket Opened(ReleasingPiiSupportTicketOpened @event, ReleasingPiiSupportTicket? current, EventContext context) => new(context.EventSourceId.Value, @event.CustomerId, @event.RequesterName);}RequesterName is [PII]-marked explicitly on the read model — required for a reducer, since Chronicle can’t infer PII lineage through arbitrary reducer logic the way it can for a model-bound projection. CustomerId carries [Subject]: the ticket’s own Id identifies the ticket, not the person the PII belongs to, so Release needs to be told which property to use as the encryption key’s owner. A property having the Subject type does not select it by itself; the attribute is what declares the role.
Release a single instance
Section titled “Release a single instance”using Cratis.Chronicle;
public class ReleasingPiiSupportTicketService(IEventStore eventStore){ public Task<ReleasingPiiSupportTicket> Release(ReleasingPiiSupportTicket ticket) => eventStore.ReadModels.Release(ticket);}Release a collection
Section titled “Release a collection”Releasing more than one instance at once resolves the subject for each independently, so a single batch can freely mix data belonging to different people.
using Cratis.Chronicle;
public class ReleasingPiiSupportTicketBatchService(IEventStore eventStore){ public Task<IEnumerable<ReleasingPiiSupportTicket>> ReleaseAll(IEnumerable<ReleasingPiiSupportTicket> tickets) => eventStore.ReadModels.Release(tickets);}Release while watching for changes
Section titled “Release while watching for changes”using Cratis.Chronicle;
public class ReleasingPiiSupportTicketWatcher(IEventStore eventStore){ public IDisposable Start() => eventStore.ReadModels.Watch<ReleasingPiiSupportTicket>().Subscribe(async changeset => { if (changeset.Removed || changeset.ReadModel is null) { return; }
var ticket = await eventStore.ReadModels.Release(changeset.ReadModel); Console.WriteLine($"{changeset.ModelKey}: {ticket.RequesterName}"); });}Every other read on IReadModels releases before returning to you. Watch is the exception, because it’s a live feed rather than one completed read — release each change yourself as it arrives, the same way you’d release any other instance.
How the subject is resolved
Section titled “How the subject is resolved”Release looks at the instance you hand it to work out whose encryption key to use, in this order:
| Priority | Mechanism |
|---|---|
| 1 | A property decorated with [Subject] |
| 2 | A constructor parameter decorated with [Subject] (record shorthand) |
| 3 | A property named Id, case-insensitive |
The first match with a value wins. If an attributed property is null, empty or Subject.NotSet, Release continues to the Id fallback. This lets older rows and partially populated instances remain releasable while a newly projected [Subject] property is introduced.
If none of the three resolve to a value, Release returns the instance unchanged and logs a warning when the model has PII metadata. If the read model has no PII-annotated properties, it returns the instance unchanged without a warning because there is nothing to release.
Add [Subject] when the identity that manual Release should use differs from the read model’s own key, exactly as you would on a command or event property to control which identity an append is encrypted under. When the two already match — a person’s own profile, keyed by their own identity — the Id fallback handles it and no attribute is needed.
This attribute affects only subject discovery from the object passed to Release; it does not assign ownership to a managed projection document. Chronicle’s projection pipeline derives ownership from the event that supplied each PII value and persists that information in its reserved __subject and __subjects fields. An explicit subject supplied while appending an event therefore controls the values originating from that event, regardless of whether the read model exposes a Subject property.
When release can’t recover a value
Section titled “When release can’t recover a value”A property that can’t be decrypted — its encryption key was deleted, or it was encrypted under a different subject entirely — degrades on its own; the rest of the instance still comes back intact. What a caller sees for each of those cases is covered in Read models and PII.
If the release call itself can’t complete — a schema mismatch, for instance — Chronicle logs the failure and returns the instance exactly as you passed it in, still encrypted. If a value you expect to read back stays ciphertext after calling Release, check the application logs for that failure before assuming the data is unrecoverable.
Related topics
Section titled “Related topics”- Read models and PII - How Chronicle encrypts and decrypts PII automatically, and GDPR erasure
- The PII attribute - Marking event and read model properties as PII
- Watching Read Models - Subscribing to read model changes
- Getting a Single Instance - The strongly consistent read path that releases for you