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);import io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
@EventTypedata class ConstraintsModelBoundUniqueProjectCreated(@Unique val name: String, val description: String)import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
@EventTyperecord ConstraintsModelBoundUniqueProjectCreated(@Unique String name, String description) {}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);import io.cratis.chronicle.concepts.ConceptAsimport io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
data class ConstraintsModelBoundUniqueEmailAddress(override val value: String) : ConceptAs<String>
@EventTypedata class ConstraintsModelBoundUniqueAuthorRegistered( @Unique(id = "UniqueAuthorEmail") val email: ConstraintsModelBoundUniqueEmailAddress)import io.cratis.chronicle.concepts.ConceptAs;import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
record ConstraintsModelBoundUniqueEmailAddress(String value) implements ConceptAs<String> { @Override public String getValue() { return value; }}
@EventTyperecord ConstraintsModelBoundUniqueAuthorRegistered( @Unique(id = "UniqueAuthorEmail") ConstraintsModelBoundUniqueEmailAddress email) {}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.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);import io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
@EventTypedata class ConstraintsModelBoundUniqueUserRegistered( @Unique(id = "UniqueEmail") val email: String, val displayName: String)
@EventTypedata class ConstraintsModelBoundUniqueUserEmailChanged(@Unique(id = "UniqueEmail") val newEmail: String)import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
@EventTyperecord ConstraintsModelBoundUniqueUserRegistered(@Unique(id = "UniqueEmail") String email, String displayName) {}
@EventTyperecord ConstraintsModelBoundUniqueUserEmailChanged(@Unique(id = "UniqueEmail") String newEmail) {}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);import io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
@EventTypedata class ConstraintsModelBoundUniqueMessageProjectCreated( @Unique(message = "A project with this name already exists.") val name: String, val description: String)import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
@EventTyperecord ConstraintsModelBoundUniqueMessageProjectCreated( @Unique(message = "A project with this name already exists.") String name, String description) {}defmodule MyApp.Events.ConstraintsModelBoundUniqueMessageProjectCreated do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-message-project-created"
defstruct [:name, :description]
unique(:name, message: "A project with this name already exists.")endTypeScript does not support this workflow yet.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);import io.cratis.chronicle.constraints.RemoveConstraintimport io.cratis.chronicle.events.EventType
@EventType@RemoveConstraint("UniqueEmail")data class ConstraintsModelBoundUniqueUserRemoved(val userId: String)import io.cratis.chronicle.constraints.RemoveConstraint;import io.cratis.chronicle.events.EventType;
@EventType@RemoveConstraint("UniqueEmail")record ConstraintsModelBoundUniqueUserRemoved(String userId) {}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);import io.cratis.chronicle.constraints.RemoveConstraintimport io.cratis.chronicle.events.EventType
@EventType@RemoveConstraint("UniqueEmail")@RemoveConstraint("UniqueUsername")data class ConstraintsModelBoundUniqueMultiRemoveUserRemoved(val userId: String)import io.cratis.chronicle.constraints.RemoveConstraint;import io.cratis.chronicle.events.EventType;
@EventType@RemoveConstraint("UniqueEmail")@RemoveConstraint("UniqueUsername")record ConstraintsModelBoundUniqueMultiRemoveUserRemoved(String userId) {}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.The reverse also holds: more than one event type can release the same constraint. Name it from each event that ends the lifecycle and every one of them frees the held values:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType][RemoveConstraint("UniqueInvitedEmail")]public record ConstraintsModelBoundUniqueSeveralInvitationAccepted;
[EventType][RemoveConstraint("UniqueInvitedEmail")]public record ConstraintsModelBoundUniqueSeveralInvitationRevoked;
[EventType][RemoveConstraint("UniqueInvitedEmail")]public record ConstraintsModelBoundUniqueSeveralInvitationExpired;Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsModelBoundUniqueSeveralInvitationAccepted do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-several-invitation-accepted"
defstruct []
remove_constraint("UniqueInvitedEmail")end
defmodule MyApp.Events.ConstraintsModelBoundUniqueSeveralInvitationRevoked do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-several-invitation-revoked"
defstruct []
remove_constraint("UniqueInvitedEmail")end
defmodule MyApp.Events.ConstraintsModelBoundUniqueSeveralInvitationExpired do use Chronicle.Events.EventType, id: "constraints-model-bound-unique-several-invitation-expired"
defstruct []
remove_constraint("UniqueInvitedEmail")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.