Table of Contents

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.