Skip to content

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.

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

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

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

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

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

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."));
}

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."));
}

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.