---
title: Releasing PII
description: Decrypt PII in a read model instance you already have, and how Chronicle decides whose encryption key to use.
---

import { Tabs, TabItem } from '@astrojs/starlight/components';


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](/chronicle/compliance/read-models/) 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.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

`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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;

public class ReleasingPiiSupportTicketService(IEventStore eventStore)
{
    public Task<ReleasingPiiSupportTicket> Release(ReleasingPiiSupportTicket ticket) =>
        eventStore.ReadModels.Release(ticket);
}
```

</TabItem>
</Tabs>

## 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.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;

public class ReleasingPiiSupportTicketBatchService(IEventStore eventStore)
{
    public Task<IEnumerable<ReleasingPiiSupportTicket>> ReleaseAll(IEnumerable<ReleasingPiiSupportTicket> tickets) =>
        eventStore.ReadModels.Release(tickets);
}
```

</TabItem>
</Tabs>

## Release while watching for changes

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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}");
        });
}
```

</TabItem>
</Tabs>

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

`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

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](/chronicle/compliance/read-models/#one-property-never-fails-the-whole-read).

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

- [Read models and PII](/chronicle/compliance/read-models/) - How Chronicle encrypts and decrypts PII automatically, and GDPR erasure
- [The PII attribute](/chronicle/compliance/pii/) - Marking event and read model properties as PII
- [Watching Read Models](/chronicle/read-models/watching-read-models/) - Subscribing to read model changes
- [Getting a Single Instance](/chronicle/read-models/getting-single-instance/) - The strongly consistent read path that releases for you
