Unique property constraint
Adorn a property with [Unique] to enforce that the value of that property is unique across every event of this type in the event store.
Chronicle discovers [Unique]-adorned properties automatically and registers the constraints with the Kernel when the client starts.
Defining a unique property constraint
Section titled “Defining a unique property constraint”using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsModelBoundUniqueProjectCreated([property: Unique] string Name, string Description);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsModelBoundUniqueProjectCreated do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-project-created"
defstruct [:name, :description]
unique(:name)endTypeScript does not support this workflow yet.Strongly-typed properties
Section titled “Strongly-typed properties”[Unique] works on ConceptAs<T> properties — the idiomatic way to model a domain value in Cratis. Chronicle treats the concept as the underlying primitive it wraps (string, Guid, and so on), so a uniqueness rule on a strongly-typed EmailAddress or OrganizationNumber behaves exactly like one on the raw primitive, and registers and enforces identically through the declarative form too:
using Cratis.Concepts;using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
public record ConstraintsModelBoundUniqueEmailAddress(string Value) : ConceptAs<string>(Value);
[EventType]public record ConstraintsModelBoundUniqueAuthorRegistered([property: Unique(name: "UniqueAuthorEmail")] ConstraintsModelBoundUniqueEmailAddress Email);Grouping constraints across event types
Section titled “Grouping constraints across event types”When multiple event types share the same constraint name, Chronicle groups them into a single constraint. A value introduced by any of the participating events is checked against all others:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsModelBoundUniqueUserRegistered([property: Unique(name: "UniqueEmail")] string Email, string DisplayName);
[EventType]public record ConstraintsModelBoundUniqueUserEmailChanged([property: Unique(name: "UniqueEmail")] string NewEmail);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsModelBoundUniqueUserRegistered do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-user-registered"
defstruct [:email, :display_name]
unique(:email, name: "UniqueEmail")end
defmodule MyApp.Events.ConstraintsModelBoundUniqueUserEmailChanged do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-user-email-changed"
defstruct [:new_email]
unique(:new_email, name: "UniqueEmail")endTypeScript does not support this workflow yet.Both events now participate in the same UniqueEmail constraint, so neither UserRegistered nor UserEmailChanged can introduce an email address that already exists.
Violation message
Section titled “Violation message”The message parameter is optional. Chronicle produces a default violation message when one is not supplied. A custom violation message is currently C#-only — none of the other clients’ constraint declarations (model-bound or declarative) support one:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsModelBoundUniqueMessageProjectCreated([property: Unique(message: "A project with this name already exists.")] string Name, string Description);Releasing a constraint
Section titled “Releasing a constraint”Apply [RemoveConstraint] to the event type that signals a domain object has been removed. When this event is appended, Chronicle releases the named constraint and its previously held values can be claimed again:
using Cratis.Concepts;using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
public record ConstraintsModelBoundUniqueUserId(Guid Value) : ConceptAs<Guid>(Value);
[EventType][RemoveConstraint("UniqueEmail")]public record ConstraintsModelBoundUniqueUserRemoved(ConstraintsModelBoundUniqueUserId UserId);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsModelBoundUniqueUserRemoved do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-user-removed"
defstruct [:user_id]
remove_constraint("UniqueEmail")endTypeScript does not support this workflow yet.The constraint name must exactly match the name used in the [Unique] attribute that established it.
An event type can release more than one constraint by stacking multiple attributes:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType][RemoveConstraint("UniqueEmail")][RemoveConstraint("UniqueUsername")]public record ConstraintsModelBoundUniqueMultiRemoveUserRemoved(ConstraintsModelBoundUniqueUserId UserId);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsModelBoundUniqueMultiRemoveUserRemoved do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-multi-remove-user-removed"
defstruct [:user_id]
remove_constraint("UniqueEmail") remove_constraint("UniqueUsername")endTypeScript does not support this workflow yet.How constraints are enforced
Section titled “How constraints are enforced”The Chronicle Kernel reads the [Unique] and [RemoveConstraint] attributes when the client connects and creates the indexes it needs. Every subsequent append is checked against those indexes server-side, so no constraint logic runs in client code.