Skip to content

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.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Events.Constraints;
[EventType]
public record ConstraintsModelBoundUniqueProjectCreated([property: Unique] string Name, string Description);

[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);

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);

Both events now participate in the same UniqueEmail constraint, so neither UserRegistered nor UserEmailChanged can introduce an email address that already exists.

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);

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);

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);

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.