Clearing Values
Some facts take a value away without putting anything in its place. A note is deleted, a due date is dropped, a shift is released. ClearWith declares which event does that, and the projection writes the member back to null every time the event is observed — including when the read model is rebuilt from the beginning of the stream.
Basic Usage
Section titled “Basic Usage”Put ClearWith on the member the event empties. The member has to be nullable.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbClearingProjectNoted(string Note);
[EventType]public record MbClearingProjectNoteCleared;
[FromEvent<MbClearingProjectNoted>]public record MbClearingProjectNotes( [Key] Guid Id,
[ClearWith<MbClearingProjectNoteCleared>] string? Note);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.SetValueimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbClearingProjectNoted(val note: String)
@EventTypedata class MbClearingProjectNoteCleared(val placeholder: Boolean = true)
@ReadModel@FromEvent(MbClearingProjectNoted::class)data class MbClearingProjectNotes( @SetFrom("note", MbClearingProjectNoted::class) @SetValue(MbClearingProjectNoteCleared::class, clear = true) val note: String? = null)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.SetValue;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbClearingProjectNoted(String note) {}
@EventTyperecord MbClearingProjectNoteCleared() {}
@ReadModel@FromEvent(eventType = MbClearingProjectNoted.class)class MbClearingProjectNotes { @SetFrom(propertyPath = "note", eventType = MbClearingProjectNoted.class) @SetValue(eventType = MbClearingProjectNoteCleared.class, clear = true) public String note;}Elixir does not support this workflow yet.import { clearWith, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class MbClearingProjectNoted { note = '';}
@eventType()class MbClearingProjectNoteCleared {}
@readModel()@fromEvent(MbClearingProjectNoted)class MbClearingProjectNotes { @setFrom(MbClearingProjectNoted, 'note') @clearWith(MbClearingProjectNoteCleared) note: string | undefined = undefined;}When MbClearingProjectNoted occurs, Note is populated. When MbClearingProjectNoteCleared occurs, Note goes back to null. A later MbClearingProjectNoted sets it again — a clear is a value written at a point in the stream, not a terminal state for the member.
The Same Clear as a Null SetValue
Section titled “The Same Clear as a Null SetValue”SetValue with null means exactly the same thing and builds exactly the same mapping. Reach for whichever reads better where you are: ClearWith states the intent, and SetValue(null) sits naturally alongside the other SetValue declarations on a member.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbClearingInvoiceIssued(string Reference);
[EventType]public record MbClearingInvoiceVoided;
[FromEvent<MbClearingInvoiceIssued>]public record MbClearingInvoice( [Key] Guid Id,
[SetValue<MbClearingInvoiceVoided>(null)] string? Reference);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetValueimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbClearingInvoiceIssued(val reference: String)
@EventTypedata class MbClearingInvoiceVoided(val placeholder: Boolean = true)
@ReadModel@FromEvent(MbClearingInvoiceIssued::class)data class MbClearingInvoice( // AutoMap sets this from the matching "reference" property on MbClearingInvoiceIssued; SetValue // with clear = true is the only way to null it back out - @ClearWith targets CLASS only, so it // cannot sit on a plain scalar property the way the .NET client's [ClearWith<T>] can. @SetValue(MbClearingInvoiceVoided::class, clear = true) val reference: String? = null)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetValue;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbClearingInvoiceIssued(String reference) {}
@EventTyperecord MbClearingInvoiceVoided() {}
@ReadModel@FromEvent(eventType = MbClearingInvoiceIssued.class)class MbClearingInvoice { // AutoMap sets this from the matching "reference" field on MbClearingInvoiceIssued; SetValue with // clear = true is the only way to null it back out - @ClearWith targets CLASS only, so it cannot // sit on a plain scalar field the way the .NET client's [ClearWith<T>] can. @SetValue(eventType = MbClearingInvoiceVoided.class, clear = true) public String reference;}Elixir does not support this workflow yet.import { eventType, fromEvent, Guid, readModel, setValue } from '@cratis/chronicle';
@eventType()class MbClearingInvoiceIssued { constructor(readonly reference: string) {}}
@eventType()class MbClearingInvoiceVoided {}
@readModel()@fromEvent(MbClearingInvoiceIssued)class MbClearingInvoice { id: Guid = Guid.empty;
@setValue(MbClearingInvoiceVoided, null) reference: string | null = null;}The Member Must Be Able to Hold No Value
Section titled “The Member Must Be Able to Hold No Value”Clearing means returning a member to no value, so the member must have that state. A non-nullable member does not, and the declaration is refused: CHR0048 reports it as a build warning — scheduled to become an error in the next major — and building the projection throws CannotClearNonNullableMember, so an unaddressed warning fails at startup.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbClearingShiftPlanned(string Assignee, int Hours);
[EventType]public record MbClearingShiftReleased;
[FromEvent<MbClearingShiftPlanned>]public record MbClearingShift( [Key] Guid Id,
// Nullable, so "nobody is assigned" is a state the member can actually hold. [ClearWith<MbClearingShiftReleased>] string? Assignee,
// Nullable value type, for the same reason: 0 hours is a number of hours, not the absence of one. [ClearWith<MbClearingShiftReleased>] int? Hours);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetValueimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbClearingShiftPlanned(val assignee: String, val hours: Int)
@EventTypedata class MbClearingShiftReleased(val placeholder: Boolean = true)
@ReadModel@FromEvent(MbClearingShiftPlanned::class)data class MbClearingShift( // Nullable, so "nobody is assigned" is a state the property can actually hold. @SetValue(MbClearingShiftReleased::class, clear = true) val assignee: String? = null,
// Nullable numeric type, for the same reason: 0 hours is a number of hours, not the absence of one. @SetValue(MbClearingShiftReleased::class, clear = true) val hours: Int? = null)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetValue;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbClearingShiftPlanned(String assignee, int hours) {}
@EventTyperecord MbClearingShiftReleased() {}
@ReadModel@FromEvent(eventType = MbClearingShiftPlanned.class)class MbClearingShift { // A reference type, so "nobody is assigned" is a state the field can actually hold. @SetValue(eventType = MbClearingShiftReleased.class, clear = true) public String assignee;
// Boxed Integer, for the same reason: 0 hours is a number of hours, not the absence of one. A // primitive int could never be cleared this way. @SetValue(eventType = MbClearingShiftReleased.class, clear = true) public Integer hours;}Elixir does not support this workflow yet.import { clearWith, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class MbClearingShiftPlanned { constructor( readonly assignee: string, readonly hours: number ) {}}
@eventType()class MbClearingShiftReleased {}
@readModel()@fromEvent(MbClearingShiftPlanned)class MbClearingShift { // Optional, so "nobody is assigned" is a state the member can actually hold. @setFrom(MbClearingShiftPlanned, 'assignee') @clearWith(MbClearingShiftReleased) assignee: string | undefined = undefined;
// Optional for the same reason: 0 hours is a number of hours, not the absence of one. @setFrom(MbClearingShiftPlanned, 'hours') @clearWith(MbClearingShiftReleased) hours: number | undefined = undefined;}This is deliberate rather than a limitation to work around. The only value a projection could write to a non-nullable member is its type default — "", 0, DateTimeOffset.MinValue — and that is a different fact. A reader seeing an empty string cannot tell whether the value was cleared or was genuinely empty, which is exactly the ambiguity a type-specific “not set” sentinel introduces.
So decide which you mean:
| You mean | Declare |
|---|---|
| No value at all | A nullable member plus [ClearWith<TEvent>] |
| A specific value that happens to be the type default | [SetValue<TEvent>("")], [SetValue<TEvent>(0)], … |
Child Items
Section titled “Child Items”A member of a child collection item clears the same way. The clearing event has to reach the child, so it needs its own ChildrenFrom with the key that identifies which item to clear.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbClearingTaskListStarted(string Name);
[EventType]public record MbClearingTaskAdded(Guid ListId, Guid TaskId, string Title, string Due);
[EventType]public record MbClearingTaskDeferred(Guid ListId, Guid TaskId);
public record MbClearingTask( [Key] Guid Id, [SetFrom<MbClearingTaskAdded>(nameof(MbClearingTaskAdded.Title))] string Title, [SetFrom<MbClearingTaskAdded>(nameof(MbClearingTaskAdded.Due))] [ClearWith<MbClearingTaskDeferred>] string? Due);
[FromEvent<MbClearingTaskListStarted>]public record MbClearingTaskList( [Key] Guid Id,
[ChildrenFrom<MbClearingTaskAdded>(key: nameof(MbClearingTaskAdded.TaskId), parentKey: nameof(MbClearingTaskAdded.ListId), identifiedBy: nameof(MbClearingTask.Id))] [ChildrenFrom<MbClearingTaskDeferred>(key: nameof(MbClearingTaskDeferred.TaskId), parentKey: nameof(MbClearingTaskDeferred.ListId), identifiedBy: nameof(MbClearingTask.Id))] IReadOnlyList<MbClearingTask> Tasks);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.SetValueimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbClearingTaskListStarted(val name: String)
@EventTypedata class MbClearingTaskAdded(val listId: String, val taskId: String, val title: String, val due: String)
@EventTypedata class MbClearingTaskDeferred(val listId: String, val taskId: String)
data class MbClearingTask( val taskId: String = "",
@SetFrom("title", MbClearingTaskAdded::class) val title: String = "",
@SetFrom("due", MbClearingTaskAdded::class) @SetValue(MbClearingTaskDeferred::class, clear = true) val due: String? = null)
@ReadModel@FromEvent(MbClearingTaskListStarted::class)data class MbClearingTaskList( @ChildrenFrom(MbClearingTaskAdded::class, key = "taskId", identifiedBy = "taskId", parentKey = "listId") @ChildrenFrom(MbClearingTaskDeferred::class, key = "taskId", identifiedBy = "taskId", parentKey = "listId") val tasks: List<MbClearingTask> = emptyList())import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.SetValue;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbClearingTaskListStarted(String name) {}
@EventTyperecord MbClearingTaskAdded(String listId, String taskId, String title, String due) {}
@EventTyperecord MbClearingTaskDeferred(String listId, String taskId) {}
class MbClearingTask { public String taskId = "";
@SetFrom(propertyPath = "title", eventType = MbClearingTaskAdded.class) public String title = "";
@SetFrom(propertyPath = "due", eventType = MbClearingTaskAdded.class) @SetValue(eventType = MbClearingTaskDeferred.class, clear = true) public String due;}
@ReadModel@FromEvent(eventType = MbClearingTaskListStarted.class)class MbClearingTaskList { @ChildrenFrom(eventType = MbClearingTaskAdded.class, key = "taskId", identifiedBy = "taskId", parentKey = "listId") @ChildrenFrom(eventType = MbClearingTaskDeferred.class, key = "taskId", identifiedBy = "taskId", parentKey = "listId") public List<MbClearingTask> tasks = Collections.emptyList();}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Nested Objects
Section titled “Nested Objects”ClearWith on a member of a nested type clears that member and leaves the object standing. ClearWith on the Nested member itself — or on the nested type, as described in Nested Objects — clears the whole object back to null.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbClearingContractSigned(string Title, string NoticeGiven);
[EventType]public record MbClearingNoticeWithdrawn;
[EventType]public record MbClearingContractEnded;
[FromEvent<MbClearingContractSigned>]public record MbClearingContract( string Title,
// Clears this member of the nested object; the object itself stays. [ClearWith<MbClearingNoticeWithdrawn>] string? NoticeGiven);
public record MbClearingEmployee( [Key] Guid Id,
// Clears the whole nested object back to null. [Nested] [ClearWith<MbClearingContractEnded>] MbClearingContract? Contract);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ClearWithimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.Nestedimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.SetValueimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbClearingContractSigned(val title: String, val noticeGiven: String)
@EventTypedata class MbClearingNoticeWithdrawn(val placeholder: Boolean = true)
@EventTypedata class MbClearingContractEnded(val placeholder: Boolean = true)
@EventTypedata class MbClearingEmployeeHired(val placeholder: Boolean = true)
@FromEvent(MbClearingContractSigned::class)@ClearWith(MbClearingContractEnded::class)data class MbClearingContract( @SetFrom("title", MbClearingContractSigned::class) val title: String = "",
// Clears this property of the nested object; the object itself stays. @SetFrom("noticeGiven", MbClearingContractSigned::class) @SetValue(MbClearingNoticeWithdrawn::class, clear = true) val noticeGiven: String? = null)
// The Kotlin client only recognizes a model-bound read model once it carries at least one root// @FromEvent - MbClearingContract's own @FromEvent above still drives the nested object.@ReadModel@FromEvent(MbClearingEmployeeHired::class)data class MbClearingEmployee( @Nested val contract: MbClearingContract? = null)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ClearWith;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.Nested;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.SetValue;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbClearingContractSigned(String title, String noticeGiven) {}
@EventTyperecord MbClearingNoticeWithdrawn() {}
@EventTyperecord MbClearingContractEnded() {}
@EventTyperecord MbClearingEmployeeHired() {}
@FromEvent(eventType = MbClearingContractSigned.class)@ClearWith(eventType = MbClearingContractEnded.class)class MbClearingContract { @SetFrom(propertyPath = "title", eventType = MbClearingContractSigned.class) public String title = "";
// Clears this field of the nested object; the object itself stays. @SetFrom(propertyPath = "noticeGiven", eventType = MbClearingContractSigned.class) @SetValue(eventType = MbClearingNoticeWithdrawn.class, clear = true) public String noticeGiven;}
// The Kotlin client only recognizes a model-bound read model once it carries at least one root// @FromEvent - MbClearingContract's own @FromEvent above still drives the nested object.@ReadModel@FromEvent(eventType = MbClearingEmployeeHired.class)class MbClearingEmployee { @Nested public MbClearingContract contract;}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Declaring it on the holding member rather than on the nested type is worth preferring when the nested type is shared: the owner names the event that ends its relationship, and the nested type does not have to know about it.
When This Is the Wrong Fit
Section titled “When This Is the Wrong Fit”- Emptying a collection.
ClearWithon aChildrenFromcollection is not a supported way to remove every item; useRemovedWithon the child, described in Removal. - Deleting the whole read model. That is a root-level
RemovedWith, not a clear. - A member that should fall back to a default rather than to nothing. Say so with
SetValue; the declaration then records the value on purpose instead of leaving a reader to guess.
In the Projection Declaration Language
Section titled “In the Projection Declaration Language”clear is a statement of its own, alongside increment, decrement and count. It names the member the event empties:
projection Notes => MbClearingProjectNotes from MbClearingProjectNoted note = note from MbClearingProjectNoteCleared clear noteA clear reaches a member of a nested object through its path — the same clear the MbClearingContract example above declares with an attribute:
projection Employees => MbClearingEmployee from MbClearingNoticeWithdrawn clear contract.noticeGivennote = null compiles to exactly the same mapping and keeps working, so existing declarations need no change. Prefer clear in new ones: assigning a value and taking one away are different acts, and spelling both with = hides that. It is also what you get back — when Chronicle generates a declaration from a projection definition it writes clear, whichever spelling the declaration was authored in. See the Projection Declaration Language for the rest of the syntax.
In the Fluent API
Section titled “In the Fluent API”Clear is an operation in its own right, alongside Set, Increment and Count. It is available everywhere Set is — at the root, inside Children, and inside Nested.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections;
[EventType]public record MbClearingFluentNoted(string Note);
[EventType]public record MbClearingFluentNoteCleared;
[EventType]public record MbClearingFluentSummarised(string Headline, string Note);
[EventType]public record MbClearingFluentSummaryNoteCleared;
[EventType]public record MbClearingFluentTaskAdded(Guid TaskId, string Title, string Note);
[EventType]public record MbClearingFluentTaskNoteCleared(Guid TaskId);
public record MbClearingFluentSummary(string Headline, string? Note);
public record MbClearingFluentTask([Key] Guid Id, string Title, string? Note);
public record MbClearingFluentProject( [Key] Guid Id, string? Note, MbClearingFluentSummary? Summary, IReadOnlyList<MbClearingFluentTask> Tasks);
public class MbClearingFluentProjectProjection : IProjectionFor<MbClearingFluentProject>{ public void Define(IProjectionBuilderFor<MbClearingFluentProject> builder) => builder .From<MbClearingFluentNoted>(_ => _ .Set(m => m.Note).To(e => e.Note)) .From<MbClearingFluentNoteCleared>(_ => _ .Clear(m => m.Note)) .Nested(m => m.Summary, summary => summary .From<MbClearingFluentSummarised>(_ => _ .Set(m => m.Headline).To(e => e.Headline) .Set(m => m.Note).To(e => e.Note)) .From<MbClearingFluentSummaryNoteCleared>(_ => _ .Clear(m => m.Note))) .Children(m => m.Tasks, tasks => tasks .IdentifiedBy(_ => _.Id) .From<MbClearingFluentTaskAdded>(_ => _ .UsingKey(e => e.TaskId) .Set(m => m.Title).To(e => e.Title) .Set(m => m.Note).To(e => e.Note)) .From<MbClearingFluentTaskNoteCleared>(_ => _ .UsingKey(e => e.TaskId) .Clear(m => m.Note)));}Kotlin does not support this workflow yet.`IFromBuilderFor`/`IChildFromBuilderFor` have no `clear()` method at all — the only clearing operationin the fluent builder is `INestedBuilderFor.clearWith()`, which clears an entire nested object, not asingle member of the root, a nested object, or a child item.Java does not support this workflow yet.`IFromBuilderFor`/`IChildFromBuilderFor` have no `clear()` method at all — the only clearing operationin the fluent builder is `INestedBuilderFor.clearWith()`, which clears an entire nested object, not asingle member of the root, a nested object, or a child item.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Set(...).ToValue(null) means the same thing and keeps working, so existing projections need no change. Prefer Clear in new code: it names the operation instead of spelling it as a set of a value that happens to be absent, and it reads the same as the ClearWith attribute.
Clear carries a default implementation that throws ClearNotSupported, so adding it to the builder interface does not break an implementation written outside Chronicle: such a builder keeps compiling and only fails if it is actually asked to clear something, which it could not have been before the member existed.
The nullable rule is enforced here too. C# cannot express “a nullable-annotated reference type” as a generic constraint — a non-nullable argument converts to a nullable parameter without complaint — so Clear cannot refuse a non-nullable member at its signature. Building the projection refuses it instead, throwing CannotClearNonNullableMember, and CHR0048 reports both Clear and ToValue(null) at build time.