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.Constraints;
public class UniqueProjectName : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ProjectCreated>(e => e.Name) .RemovedWith<ProjectRemoved>());}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.Constraints;
public class UniqueEmail : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .WithName("UniqueEmail") .On<UserRegistered>(e => e.Email) .On<UserEmailChanged>(e => e.NewEmail) .RemovedWith<UserRemoved>());}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:
public class UniqueEmail : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .WithName("UniqueEmail") .On<UserRegistered>(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:
public class UniqueOrderReference : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<OrderPlaced>(e => e.Reference) .RemovedWith<OrderCancelled>());}Ignoring casing
Section titled “Ignoring casing”Call .IgnoreCasing() to make the uniqueness check case-insensitive:
public class UniqueEmail : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<UserRegistered>(e => e.Email) .IgnoreCasing());}Violation message
Section titled “Violation message”Call .WithMessage(...) to provide a custom message when the constraint is violated:
public class UniqueProjectName : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ProjectCreated>(e => e.Name) .WithMessage("A project with this name already exists."));}Use a callback to compose the message dynamically from violation context:
public class UniqueProjectName : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ProjectCreated>(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.