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>());}import io.cratis.chronicle.constraints.RemoveConstraintimport io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
// Model-bound equivalent: the fluent unique builder has no removedWith() on the JVM.@EventTypedata class ConstraintsUniqueProjectCreated(@Unique(id = "ConstraintsUniqueProjectName") val name: String)
@EventType@RemoveConstraint("ConstraintsUniqueProjectName")class ConstraintsUniqueProjectRemovedimport io.cratis.chronicle.constraints.RemoveConstraint;import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
// Model-bound equivalent: the fluent unique builder has no removedWith() on the JVM.@EventTyperecord ConstraintsUniqueProjectCreated(@Unique(id = "ConstraintsUniqueProjectName") String name) {}
@EventType@RemoveConstraint("ConstraintsUniqueProjectName")record ConstraintsUniqueProjectRemoved() {}defmodule MyApp.Events.ConstraintsUniqueProjectCreated do use Chronicle.Events.EventType, id: "constraints-unique-project-created"
unique :name, name: "ConstraintsUniqueProjectName"
defstruct name: ""end
defmodule MyApp.Events.ConstraintsUniqueProjectRemoved do use Chronicle.Events.EventType, id: "constraints-unique-project-removed"
remove_constraint "ConstraintsUniqueProjectName"
defstruct []endimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueProjectCreated { @field(String) readonly name: string;
constructor(name: string) { this.name = name; }}
@eventType()class ConstraintsUniqueProjectRemoved {}
@constraint()class ConstraintsUniqueProjectName implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .on(ConstraintsUniqueProjectCreated, e => e.name) .removedWith(ConstraintsUniqueProjectRemoved)); }}Re-claiming a value
Section titled “Re-claiming a value”A unique property value belongs to an event source, not to a single append. The same event source may append its current value again without violating the constraint. If it claims a different value, the new value replaces its previous claim, freeing the old value for another event source.
Do not constrain a property containing the event source’s own id to prevent a second event on that source: it will re-claim its own value and pass. To allow an event type only once per event source, use a unique event type constraint instead.
Keeping every value
Section titled “Keeping every value”When one event source accumulates many identifiers, call .WithMode(UniqueConstraintMode.PerValue) to keep all of them reserved. The default, PerEventSource, keeps only the source’s current value. Per-value mode is available through the .NET client for now.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record MappingVersionRegistered(string VersionId);
[EventType]public record MappingVersionRemoved(string VersionId);
[EventType]public record MappingVersionsRemoved;
public class UniqueMappingVersionId : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .WithMode(UniqueConstraintMode.PerValue) .On<MappingVersionRegistered>(e => e.VersionId) .RemovedWith<MappingVersionRemoved>(e => e.VersionId) .RemovedWith<MappingVersionsRemoved>());}Each mapping version id remains unavailable to other event sources even after the project registers another version. The owning source may reclaim any of its values without a violation. .RemovedWith<MappingVersionRemoved>(e => e.VersionId) releases only the value carried by the removal event, provided that source owns it. .RemovedWith<MappingVersionsRemoved>() releases all values that source holds in the constraint’s scope.
For a composite value, pass removal-property expressions in the same order as the constrained properties. An event with no constrained properties present claims nothing and leaves earlier values reserved. .IgnoreCasing() applies to both claiming and releasing values.
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>());}import io.cratis.chronicle.constraints.Constraintimport io.cratis.chronicle.constraints.IConstraintimport io.cratis.chronicle.constraints.IConstraintBuilderimport io.cratis.chronicle.events.EventType
@EventTypedata class ConstraintsUniqueUserRegistered(val email: String)
@EventTypedata class ConstraintsUniqueUserEmailChanged(val newEmail: String)
// The constraint is named through @Constraint(id = ...); the fluent unique builder has no// withName() or removedWith() on the JVM.@Constraint(id = "UniqueEmail")class ConstraintsUniqueEmailAcrossEvents : IConstraint { override fun define(builder: IConstraintBuilder) { builder.unique { unique -> unique .on(ConstraintsUniqueUserRegistered::class, ConstraintsUniqueUserRegistered::email) .on(ConstraintsUniqueUserEmailChanged::class, ConstraintsUniqueUserEmailChanged::newEmail) } }}import io.cratis.chronicle.constraints.Constraint;import io.cratis.chronicle.constraints.IConstraint;import io.cratis.chronicle.constraints.IConstraintBuilder;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.UniqueConstraintBuilderJavaBridge;
@EventTyperecord ConstraintsUniqueUserRegistered(String email) {}
@EventTyperecord ConstraintsUniqueUserEmailChanged(String newEmail) {}
// The constraint is named through @Constraint(id = ...); the fluent unique builder has no// withName() or removedWith() on the JVM.@Constraint(id = "UniqueEmail")class ConstraintsUniqueEmailAcrossEvents implements IConstraint { @Override public void define(IConstraintBuilder builder) { builder.unique(unique -> { UniqueConstraintBuilderJavaBridge.on(unique, ConstraintsUniqueUserRegistered.class, "email"); UniqueConstraintBuilderJavaBridge.on(unique, ConstraintsUniqueUserEmailChanged.class, "newEmail"); }); }}defmodule MyApp.Events.ConstraintsUniqueUserRegistered do use Chronicle.Events.EventType, id: "constraints-unique-user-registered"
unique :email, name: "UniqueEmail"
defstruct email: ""end
defmodule MyApp.Events.ConstraintsUniqueUserEmailChanged do use Chronicle.Events.EventType, id: "constraints-unique-user-email-changed"
unique :new_email, name: "UniqueEmail"
defstruct new_email: ""end
defmodule MyApp.Events.ConstraintsUniqueUserRemoved do use Chronicle.Events.EventType, id: "constraints-unique-user-removed"
remove_constraint "UniqueEmail"
defstruct []endimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueUserRegistered { @field(String) readonly email: string;
constructor(email: string) { this.email = email; }}
@eventType()class ConstraintsUniqueUserEmailChanged { @field(String) readonly newEmail: string;
constructor(newEmail: string) { this.newEmail = newEmail; }}
@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));}import io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
// Model-bound equivalent: the explicit id names the constraint UniqueEmail.@EventTypedata class ConstraintsUniqueNamedUserRegistered(@Unique(id = "UniqueEmail") val email: String)import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
// Model-bound equivalent: the explicit id names the constraint UniqueEmail.@EventTyperecord ConstraintsUniqueNamedUserRegistered(@Unique(id = "UniqueEmail") String email) {}defmodule MyApp.Events.ConstraintsUniqueNamedUserRegistered do use Chronicle.Events.EventType, id: "constraints-unique-named-user-registered"
unique :email, name: "UniqueEmail"
defstruct email: ""endimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueNamedUserRegistered { @field(String) readonly email: string;
constructor(email: string) { this.email = email; }}
@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>());}import io.cratis.chronicle.constraints.RemoveConstraintimport io.cratis.chronicle.constraints.Uniqueimport io.cratis.chronicle.events.EventType
// Model-bound equivalent: appending the cancellation releases the reference for reuse.@EventTypedata class ConstraintsUniqueOrderPlaced(@Unique(id = "ConstraintsUniqueOrderReference") val reference: String)
@EventType@RemoveConstraint("ConstraintsUniqueOrderReference")class ConstraintsUniqueOrderCancelledimport io.cratis.chronicle.constraints.RemoveConstraint;import io.cratis.chronicle.constraints.Unique;import io.cratis.chronicle.events.EventType;
// Model-bound equivalent: appending the cancellation releases the reference for reuse.@EventTyperecord ConstraintsUniqueOrderPlaced(@Unique(id = "ConstraintsUniqueOrderReference") String reference) {}
@EventType@RemoveConstraint("ConstraintsUniqueOrderReference")record ConstraintsUniqueOrderCancelled() {}defmodule MyApp.Events.ConstraintsUniqueOrderPlaced do use Chronicle.Events.EventType, id: "constraints-unique-order-placed"
unique :reference, name: "ConstraintsUniqueOrderReference"
defstruct reference: ""end
defmodule MyApp.Events.ConstraintsUniqueOrderCancelled do use Chronicle.Events.EventType, id: "constraints-unique-order-cancelled"
remove_constraint "ConstraintsUniqueOrderReference"
defstruct []endimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueOrderPlaced { @field(String) readonly reference: string;
constructor(reference: string) { this.reference = reference; }}
@eventType()class ConstraintsUniqueOrderCancelled {}
@constraint()class ConstraintsUniqueOrderReference implements IConstraint { define(builder: IConstraintBuilder): void { builder.unique(unique => unique .on(ConstraintsUniqueOrderPlaced, e => e.reference) .removedWith(ConstraintsUniqueOrderCancelled)); }}A lifecycle often ends in more than one way — an invitation is over when it is accepted, revoked, or expires. Call .RemovedWith<T>() once for each of those events and every one of them releases the value on its own; declaring a second does not replace the first:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueSeveralInvitationSent(string EmailAddress);
[EventType]public record ConstraintsUniqueSeveralInvitationAccepted;
[EventType]public record ConstraintsUniqueSeveralInvitationRevoked;
[EventType]public record ConstraintsUniqueSeveralInvitationExpired;
public class ConstraintsUniqueSeveralInvitedAddress : IConstraint{ public void Define(IConstraintBuilder builder) => builder.Unique(unique => unique .On<ConstraintsUniqueSeveralInvitationSent>(e => e.EmailAddress) .RemovedWith<ConstraintsUniqueSeveralInvitationAccepted>() .RemovedWith<ConstraintsUniqueSeveralInvitationRevoked>() .RemovedWith<ConstraintsUniqueSeveralInvitationExpired>());}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsUniqueSeveralInvitationSent do use Chronicle.Events.EventType, id: "constraints-unique-several-invitation-sent" defstruct email_address: ""end
defmodule MyApp.Events.ConstraintsUniqueSeveralInvitationAccepted do use Chronicle.Events.EventType, id: "constraints-unique-several-invitation-accepted" defstruct []end
defmodule MyApp.Events.ConstraintsUniqueSeveralInvitationRevoked do use Chronicle.Events.EventType, id: "constraints-unique-several-invitation-revoked" defstruct []end
defmodule MyApp.Events.ConstraintsUniqueSeveralInvitationExpired do use Chronicle.Events.EventType, id: "constraints-unique-several-invitation-expired" defstruct []end
defmodule MyApp.Constraints.ConstraintsUniqueSeveralInvitedAddress do use Chronicle.Events.Constraint
unique name: "ConstraintsUniqueSeveralInvitedAddress" do on MyApp.Events.ConstraintsUniqueSeveralInvitationSent, [:email_address] removed_with MyApp.Events.ConstraintsUniqueSeveralInvitationAccepted removed_with MyApp.Events.ConstraintsUniqueSeveralInvitationRevoked removed_with MyApp.Events.ConstraintsUniqueSeveralInvitationExpired endendimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueSeveralInvitationSent { @field(String) readonly emailAddress: string;
constructor(emailAddress: string) { this.emailAddress = emailAddress; }}
@eventType()class ConstraintsUniqueSeveralInvitationAccepted {}
@eventType()class ConstraintsUniqueSeveralInvitationRevoked {}
@eventType()class ConstraintsUniqueSeveralInvitationExpired {}
@constraint()class ConstraintsUniqueSeveralInvitedAddress implements IConstraint { define(builder: IConstraintBuilder): void { // Each removedWith() adds a release event; all distinct types are retained. builder.unique(unique => unique .on(ConstraintsUniqueSeveralInvitationSent, e => e.emailAddress) .removedWith(ConstraintsUniqueSeveralInvitationAccepted) .removedWith(ConstraintsUniqueSeveralInvitationRevoked) .removedWith(ConstraintsUniqueSeveralInvitationExpired)); }}Scoping to the event log
Section titled “Scoping to the event log”By default a constraint applies to every event sequence its event types are appended to, and each sequence keeps its own index. When an event type is a domain fact that is also forwarded to the outbox, the forwarded copy claims the value in the outbox’s index, and only a removal event appended to the outbox can release it. Call .ForEventLog() to apply the constraint to the event log alone, or .ForEventSequences(...) to name other sequences. The kernel neither validates nor indexes the constraint for a sequence it does not apply to:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ConstraintsUniqueEventLogInvitationSent(string Email);
[EventType]public record ConstraintsUniqueEventLogInvitationRevoked;
public class ConstraintsUniqueEventLogInvitationEmail : IConstraint{ // InvitationSent is also forwarded to the outbox. Scope the constraint to the event log // so the forwarded copy does not claim the email in the outbox's own index. public void Define(IConstraintBuilder builder) => builder .ForEventLog() .Unique(unique => unique .On<ConstraintsUniqueEventLogInvitationSent>(e => e.Email) .RemovedWith<ConstraintsUniqueEventLogInvitationRevoked>());}import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueEventLogInvitationSent { @field(String) readonly email: string;
constructor(email: string) { this.email = email; }}
@eventType()class ConstraintsUniqueEventLogInvitationRevoked {}
@constraint()class ConstraintsUniqueEventLogInvitationEmail implements IConstraint { // InvitationSent is also forwarded to the outbox. Scope the constraint to the event log // so the forwarded copy does not claim the email in the outbox's own index. define(builder: IConstraintBuilder): void { builder .forEventLog() .unique(unique => unique .on(ConstraintsUniqueEventLogInvitationSent, e => e.email) .removedWith(ConstraintsUniqueEventLogInvitationRevoked)); }}The declaration covers every constraint defined on that builder, wherever in the chain it is called, and calling .ForEventSequences(...) again adds to the sequences already named. A builder without either call applies its constraints to every event sequence.
Changing the sequences takes effect when the client next registers its constraints. The kernel then rebuilds, in every namespace, the index of each sequence the constraint now applies to but did not before, from the events already in that sequence. A sequence the constraint no longer applies to stops validating it, and its index is no longer read until the constraint applies there again, when it is rebuilt.
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.Constraintimport io.cratis.chronicle.constraints.IConstraintimport io.cratis.chronicle.constraints.IConstraintBuilderimport io.cratis.chronicle.events.EventType
@EventTypedata class ConstraintsUniqueCasingUserRegistered(val email: String)
@Constraintclass ConstraintsUniqueCasingEmail : IConstraint { override fun define(builder: IConstraintBuilder) { builder.unique { unique -> unique .on(ConstraintsUniqueCasingUserRegistered::class, ConstraintsUniqueCasingUserRegistered::email) .ignoreCasing() } }}import io.cratis.chronicle.constraints.Constraint;import io.cratis.chronicle.constraints.IConstraint;import io.cratis.chronicle.constraints.IConstraintBuilder;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.UniqueConstraintBuilderJavaBridge;
@EventTyperecord ConstraintsUniqueCasingUserRegistered(String email) {}
@Constraintclass ConstraintsUniqueCasingEmail implements IConstraint { @Override public void define(IConstraintBuilder builder) { builder.unique(unique -> { UniqueConstraintBuilderJavaBridge.on(unique, ConstraintsUniqueCasingUserRegistered.class, "email") .ignoreCasing(); }); }}defmodule MyApp.Events.ConstraintsUniqueCasingUserRegistered do use Chronicle.Events.EventType, id: "constraints-unique-casing-user-registered"
unique :email, ignore_casing: true
defstruct email: ""endimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueCasingUserRegistered { @field(String) readonly email: string;
constructor(email: string) { this.email = email; }}
@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."));}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.constraints.Constraintimport io.cratis.chronicle.constraints.IConstraintimport io.cratis.chronicle.constraints.IConstraintBuilderimport io.cratis.chronicle.events.EventType
@EventTypedata class ConstraintsUniqueMessageProjectCreated(val name: String)
@Constraintclass ConstraintsUniqueMessageProjectName : IConstraint { override fun define(builder: IConstraintBuilder) { builder.unique { unique -> unique .on(ConstraintsUniqueMessageProjectCreated::class, ConstraintsUniqueMessageProjectCreated::name) .withMessage("A project with this name already exists.") } }}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.constraints.Constraint;import io.cratis.chronicle.constraints.IConstraint;import io.cratis.chronicle.constraints.IConstraintBuilder;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.UniqueConstraintBuilderJavaBridge;
@EventTyperecord ConstraintsUniqueMessageProjectCreated(String name) {}
@Constraintclass ConstraintsUniqueMessageProjectName implements IConstraint { @Override public void define(IConstraintBuilder builder) { builder.unique(unique -> { UniqueConstraintBuilderJavaBridge.on(unique, ConstraintsUniqueMessageProjectCreated.class, "name") .withMessage("A project with this name already exists."); }); }}defmodule MyApp.Events.ConstraintsUniqueMessageProjectCreated do use Chronicle.Events.EventType, id: "constraints-unique-message-project-created" defstruct name: ""end
defmodule MyApp.Constraints.ConstraintsUniqueMessageProjectName do use Chronicle.Events.Constraint
unique name: "ConstraintsUniqueMessageProjectName", message: "A project with this name already exists." do on MyApp.Events.ConstraintsUniqueMessageProjectCreated, [:name] endendimport { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ConstraintsUniqueMessageProjectCreated { @field(String) readonly name: string;
constructor(name: string) { this.name = name; }}
@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."));}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstraintsUniqueMessageCallbackProjectCreated do use Chronicle.Events.EventType, id: "constraints-unique-message-callback-project-created" defstruct name: ""end
defmodule MyApp.Constraints.ConstraintsUniqueMessageCallbackMessages do alias Chronicle.Events.ConstraintViolation
def project_name_taken(violation) do "A project named '#{ConstraintViolation.details(violation)[:property_value]}' already exists." endend
defmodule MyApp.Constraints.ConstraintsUniqueMessageCallbackProjectName do use Chronicle.Events.Constraint
unique name: "ConstraintsUniqueMessageCallbackProjectName", message: {MyApp.Constraints.ConstraintsUniqueMessageCallbackMessages, :project_name_taken} do on MyApp.Events.ConstraintsUniqueMessageCallbackProjectCreated, [:name] endendTypeScript does not support this workflow yet.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.
Rebuilding an index
Section titled “Rebuilding an index”When a constraint changes, Chronicle rebuilds its index from the event sequence in log order. It clears all claims in each scope before replaying that scope’s first event, so stale claims do not block historical values.
If the store rejects a duplicate claim during the rebuild, Chronicle logs a warning, skips that claim, and continues. The first historical claimant keeps the value until an event releases or replaces it. A live append during the rebuild can claim a value before its historical claimant is replayed; the log-order rule applies to history, not to concurrent live appends.