Unique property constraint
Use builder.Unique(...) inside an IConstraint implementation to enforce that a property value is unique across one or more event types. The constraint fires if any tracked event would introduce a duplicate value.
Chronicle discovers all IConstraint implementations automatically — no registration is needed.
Defining a constraint
Section titled “Defining a constraint”Implement IConstraint and call the builder in Define:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueProjectCreated(string Name);
[EventType]public record ConstraintsUniqueProjectRemoved;
public class ConstraintsUniqueProjectName : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ConstraintsUniqueProjectCreated>(e => e.Name) .RemovedWith<ConstraintsUniqueProjectRemoved>());}Kotlin does not support this workflow yet.Java does not support this workflow yet.import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
@eventType()class ConstraintsUniqueProjectCreated { constructor(readonly name: string) {}}
@eventType()class ConstraintsUniqueProjectRemoved {}
@constraint()class ConstraintsUniqueProjectName implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .on(ConstraintsUniqueProjectCreated, e => e.name) .removedWith(ConstraintsUniqueProjectRemoved)); }}Uniqueness across multiple event types
Section titled “Uniqueness across multiple event types”Use multiple .On calls when several event types each contribute to the same logical uniqueness rule. The constraint fires if any of the tracked events would introduce a duplicate value:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueUserRegistered(string Email);
[EventType]public record ConstraintsUniqueUserEmailChanged(string NewEmail);
[EventType]public record ConstraintsUniqueUserRemoved;
public class ConstraintsUniqueEmailAcrossEvents : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .WithName("UniqueEmail") .On<ConstraintsUniqueUserRegistered>(e => e.Email) .On<ConstraintsUniqueUserEmailChanged>(e => e.NewEmail) .RemovedWith<ConstraintsUniqueUserRemoved>());}Kotlin does not support this workflow yet.Java does not support this workflow yet.import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
@eventType()class ConstraintsUniqueUserRegistered { constructor(readonly email: string) {}}
@eventType()class ConstraintsUniqueUserEmailChanged { constructor(readonly newEmail: string) {}}
@eventType()class ConstraintsUniqueUserRemoved {}
@constraint()class ConstraintsUniqueEmailAcrossEvents implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .withName('UniqueEmail') .on(ConstraintsUniqueUserRegistered, e => e.email) .on(ConstraintsUniqueUserEmailChanged, e => e.newEmail) .removedWith(ConstraintsUniqueUserRemoved)); }}Constraint name
Section titled “Constraint name”Use .WithName(...) to give the constraint an explicit name. When not provided, Chronicle uses the class name of the IConstraint implementation:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueNamedUserRegistered(string Email);
public class ConstraintsUniqueNamedEmail : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .WithName("UniqueEmail") .On<ConstraintsUniqueNamedUserRegistered>(e => e.Email));}Kotlin does not support this workflow yet.Java does not support this workflow yet.import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
@eventType()class ConstraintsUniqueNamedUserRegistered { constructor(readonly email: string) {}}
@constraint()class ConstraintsUniqueNamedEmail implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .withName('UniqueEmail') .on(ConstraintsUniqueNamedUserRegistered, e => e.email)); }}Releasing a constraint
Section titled “Releasing a constraint”Call .RemovedWith<T>() to register the event type that releases the constraint. When that event is appended, the previously held value is freed and can be claimed again:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueOrderPlaced(string Reference);
[EventType]public record ConstraintsUniqueOrderCancelled;
public class ConstraintsUniqueOrderReference : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ConstraintsUniqueOrderPlaced>(e => e.Reference) .RemovedWith<ConstraintsUniqueOrderCancelled>());}Kotlin does not support this workflow yet.Java does not support this workflow yet.import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
@eventType()class ConstraintsUniqueOrderPlaced { constructor(readonly reference: string) {}}
@eventType()class ConstraintsUniqueOrderCancelled {}
@constraint()class ConstraintsUniqueOrderReference implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .on(ConstraintsUniqueOrderPlaced, e => e.reference) .removedWith(ConstraintsUniqueOrderCancelled)); }}Ignoring casing
Section titled “Ignoring casing”Call .IgnoreCasing() to make the uniqueness check case-insensitive:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueCasingUserRegistered(string Email);
public class ConstraintsUniqueCasingEmail : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ConstraintsUniqueCasingUserRegistered>(e => e.Email) .IgnoreCasing());}import io.cratis.chronicle.constraints.IConstraintimport io.cratis.chronicle.constraints.IConstraintBuilderimport io.cratis.chronicle.events.EventType
@EventType(id = "constraints-unique-casing-user-registered")data class ConstraintsUniqueCasingUserRegistered(val email: String)
class ConstraintsUniqueCasingEmail : IConstraint { override fun define(builder: IConstraintBuilder) { builder.unique { unique -> unique .on(ConstraintsUniqueCasingUserRegistered::class) { it.email } .ignoreCasing() } }}Java does not support this workflow yet.import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
@eventType()class ConstraintsUniqueCasingUserRegistered { constructor(readonly email: string) {}}
@constraint()class ConstraintsUniqueCasingEmail implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .on(ConstraintsUniqueCasingUserRegistered, e => e.email) .ignoreCasing()); }}Violation message
Section titled “Violation message”Call .WithMessage(...) to provide a custom message when the constraint is violated:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueMessageProjectCreated(string Name);
public class ConstraintsUniqueMessageProjectName : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ConstraintsUniqueMessageProjectCreated>(e => e.Name) .WithMessage("A project with this name already exists."));}import io.cratis.chronicle.constraints.IConstraintimport io.cratis.chronicle.constraints.IConstraintBuilderimport io.cratis.chronicle.events.EventType
@EventType(id = "constraints-unique-message-project-created")data class ConstraintsUniqueMessageProjectCreated(val name: String)
class ConstraintsUniqueMessageProjectName : IConstraint { override fun define(builder: IConstraintBuilder) { builder.unique { unique -> unique .on(ConstraintsUniqueMessageProjectCreated::class) { it.name } .withMessage("A project with this name already exists.") } }}Java does not support this workflow yet.import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
@eventType()class ConstraintsUniqueMessageProjectCreated { constructor(readonly name: string) {}}
@constraint()class ConstraintsUniqueMessageProjectName implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .on(ConstraintsUniqueMessageProjectCreated, e => e.name) .withMessage('A project with this name already exists.')); }}Use a callback to compose the message dynamically from violation context:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueMessageCallbackProjectCreated(string Name);
public class ConstraintsUniqueMessageCallbackProjectName : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ConstraintsUniqueMessageCallbackProjectCreated>(e => e.Name) .WithMessage(violation => $"A project named '{violation.Details[WellKnownConstraintDetailKeys.PropertyValue]}' already exists."));}How constraints are enforced
Section titled “How constraints are enforced”When a constraint is registered, the Chronicle Kernel creates the indexes required to enforce it. Constraints are evaluated server-side during append, ensuring data integrity regardless of the client.