| name | add-business-rule |
| description | Use this skill when asked to add a validation rule, business rule, or uniqueness constraint to an existing command in a Cratis-based project. |
Add a business rule or event-store constraint to an existing command.
Choose the right mechanism
| Scenario | Use |
|---|
| Single-event uniqueness (most cases) | [Unique] attribute on the event type |
Multi-event uniqueness or needs RemovedWith | IConstraint |
| Rule requiring Chronicle event-sourced state | ReadModel as Handle() parameter (DCB) |
| Simple sync invariant (format, range, required) | CommandValidator<T> |
Business Rules via DCB (ReadModel as argument)
Use when the rule depends on Chronicle event-sourced state (e.g. current count, accumulated value).
This is the DCB (Dynamic Consistency Boundary) pattern: add a read model parameter to the Handle() method.
The framework fetches and injects the current read model snapshot before Handle() runs.
The read model must already exist in the slice (decorated with [ReadModel] and a model-bound projection).
If it doesn't, add it first — see the new-vertical-slice skill.
[Command]
public record <Command>(<KeyProperty> <Key>, ...)
{
public <Event> Handle(<ReadModel> <readModelParam>)
{
if (<readModelParam>.<StateProperty> <violatesRule>)
throw new <ViolationException>(<meaningful message>);
return new <Event>(...);
}
}
Example — limit items in cart to 3:
[Command]
public record AddItemToCart(CartId CartId, ItemId ItemId)
{
public ItemAddedToCart Handle(CartSummary cart)
{
if (cart.ItemCount >= 3)
throw new CartIsFullException(CartId);
return new ItemAddedToCart(ItemId);
}
}
Key rules:
- The read model parameter type must match a
[ReadModel]-decorated type in the same feature.
- The framework resolves the instance using the same event-source key as the command.
- Throw a custom exception (never a built-in one) to signal violation — the framework converts it to an error result.
- One parameter per logical read model; if you need multiple reads, use multiple parameters.
Event-Store Constraints — [Unique] attribute (preferred)
For the common case of enforcing uniqueness on a single event type, adorn the event type class or one of its properties with [Unique]. No separate constraint class is needed.
Event-type uniqueness — only one event of this type per event source:
[EventType]
[Unique(message: "A project with this name already exists.")]
public record ProjectCreated(string Name, string Description);
Property uniqueness — the value of a specific property must be unique:
[EventType]
public record UserRegistered([Unique(name: "UniqueEmail", message: "Email already registered.")] string Email, string DisplayName);
Releasing a constraint — apply [RemoveConstraint] to the event that deletes the domain object:
[EventType]
[RemoveConstraint("UniqueEmail")]
public record UserRemoved(UserId UserId);
Multiple attributes can be stacked to release more than one constraint from the same event type.
Constraints are discovered and enforced automatically — no registration or attribute on the command needed.
Event-Store Constraints — IConstraint (advanced)
Use IConstraint when the uniqueness rule spans event types with different property names, or when a RemovedWith event must release the constraint.
public class Unique<PropertyName> : IConstraint
{
public void Define(IConstraintBuilder builder) =>
builder.Unique(unique =>
unique
.On<<EventType>>(e => e.<UniqueProperty>)
.RemovedWith<<RemovedEventType>>());
}
Example — unique project name with removal:
public class UniqueProjectName : IConstraint
{
public void Define(IConstraintBuilder builder) =>
builder.Unique(unique =>
unique
.On<ProjectRegistered>(e => e.Name)
.RemovedWith<ProjectRemoved>());
}
Constraints are discovered and enforced automatically — no registration or attribute on the command needed.
After adding
- Add a spec for the failure case — see
write-specs skill
- Run
dotnet build and dotnet test
- Fix all failures before completing