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
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
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
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
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
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
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.Value}' already exists."));
}
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.