一键导入
add-business-rule
Use this skill when asked to add a validation rule, business rule, or uniqueness constraint to an existing command in a Cratis-based project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use this skill when asked to add a validation rule, business rule, or uniqueness constraint to an existing command in a Cratis-based project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use this skill when asked to add a Chronicle reactor (automation or translation) to a Cratis-based project. Reactors observe events and produce side effects.
Ship staged or unstaged local changes: create a branch, make logical commits, push to origin, open a PR with the correct description and label, merge it, and delete the branch locally and on origin. Use whenever the user asks to commit, push, create a PR, ship, or land changes.
Use this skill when asked to create a strongly-typed domain identifier or value (such as ProjectId, AuthorName, InvoiceNumber) in a Cratis-based project. Produces a ConceptAs<T> record with the correct conversions and sentinel values.
Use this skill when creating a NEW Cratis documentation page under `Documentation/**` (tutorial, how-to, explanation, reference, or recipe). Handles where the file goes (which product repo), sidebar wiring (toc.yml + Diátaxis bucket), and verifying it renders. Trigger on add/create/write a new docs page, document a new feature, or add a new section to a product's docs.
Use this skill when asked to add a database table, column, relationship, or other schema change via Entity Framework Core in a Cratis-based project.
Use this skill when asked to add a Chronicle projection to a Cratis-based project. Favor model-bound projections by default, and only fall back to declarative/fluent `IProjectionFor<T>` projections when model-bound attributes cannot express the behavior cleanly. Enforces the AutoMap-first rule and Chronicle-specific join semantics.
| 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.
Pick by what the decision is — see vertical-slices.md "The decision matrix":
| Scenario | Use |
|---|---|
| Reusable value invariant (length, format, range) | ConceptValidator<T> on the concept type |
| Command-input / cross-field / pre-handler rule (incl. injected read-model/service checks) | CommandValidator<TCommand> with RuleFor(...) |
| Handler needs fetched/computed data before it can build the event | Provide() — fetch the data; short-circuit with ValidationResult.Error(...) if unusable |
| State-dependent rule that must hold under concurrency | inject the read model into Handle(), return Result<TEvent, ValidationResult> |
| Single-event uniqueness (most cases) | [Unique] attribute on the event type |
Multi-event uniqueness or needs RemovedWith | IConstraint |
| Genuinely exceptional failure (bug, missing infra) | throw a custom domain exception |
Never throw for normal business rejection. A thrown exception from
Provide()/Handle()surfaces asHasExceptions/ HTTP 500 — not a validation result. Recoverable, user-facing rejections are validation: return them via a validator,Provide(), orResult<TEvent, ValidationResult>.
Handle() argument)Use when the rule depends on Chronicle event-sourced state (current count, accumulated value) and must hold under concurrency. The framework injects the current read-model snapshot, resolved by the command's event-source id, before Handle() runs. Return a Result<TEvent, ValidationResult> — success carries the event, failure carries a typed validation error.
The read model must already exist in the slice ([ReadModel] + a model-bound projection). If it doesn't, add it first — see the add-projection / cratis-readmodel skills.
[Command]
public record AddItemToCart(CartId CartId, ItemId ItemId)
{
/// <summary>Adds the item; rejects when the cart already holds the maximum.</summary>
/// <param name="cart">The current cart summary (injected by event-source id).</param>
/// <returns>The event on success, or a validation error.</returns>
public Result<ItemAddedToCart, ValidationResult> Handle(CartSummary cart) =>
cart.ItemCount >= 3
? ValidationResult.Error("A cart can hold at most 3 items.")
: new ItemAddedToCart(ItemId);
}
Key rules:
[ReadModel] in the same slice/feature; the framework resolves the instance by the command's event-source key. To read a read model keyed differently, use Provide() with IReadModels.GetInstanceById<T>((EventSourceId)key).Result<TEvent, ValidationResult> — never throw for the rejection.CommandValidator<T>For rules that don't depend on race-sensitive state, put them in the validator that sits beside the command. Constructor dependencies (read models, services) are injected and resolved by the command's event-source id.
public class TransferFundsValidator : CommandValidator<TransferFunds>
{
public TransferFundsValidator() =>
RuleFor(c => c.Amount).GreaterThan(0).WithMessage("Amount must be positive.");
}
Single-property intrinsic rules belong on ConceptValidator<T> instead, so they travel with the value everywhere.
[Unique] (preferred)For uniqueness on a single event type, adorn the event type or one of its properties with [Unique]. No separate constraint class is needed; constraints are discovered automatically.
// Event-type uniqueness — only one of this event per event source
[EventType]
[Unique(message: "A project with this name already exists.")]
public record ProjectRegistered(ProjectName Name);
// Property uniqueness — the value must be unique across event sources
[EventType]
public record UserRegistered([Unique(name: "UniqueEmail", message: "Email already registered.")] EmailAddress Email, DisplayName Name);
// Release a claimed value on removal — the name must match the [Unique] name exactly
[EventType]
[RemoveConstraint("UniqueEmail")]
public record UserRemoved;
Stack multiple [RemoveConstraint("...")] attributes on one removal event to release several claimed values at once. To enforce the same property across multiple event types, give each its [Unique(name: "UniqueEmail")] with the same name.
IConstraint (advanced)Use IConstraint when uniqueness spans event types with different property names, needs .IgnoreCasing(), or a RemovedWith event must release it. Define is declarative — member-access lambdas only, no DI or side effects.
public class UniqueProjectName : IConstraint
{
public void Define(IConstraintBuilder builder) =>
builder.Unique(unique =>
unique
.On<ProjectRegistered>(e => e.Name)
.IgnoreCasing() // case-insensitive — do NOT lowercase inside the lambda
.RemovedWith<ProjectRemoved>()); // omit if there is no remove event
}
.On<TEvent>(e => e.Prop) lambda is parsed for member access only — e => e.Email.ToLower() is a pitfall; use .IgnoreCasing().builder.Unique<TEvent>(name:, message:) enforces "one event of this type per event source" via the fluent builder (distinct from class-level [Unique]).Spec the constraint with EventScenario: seed the conflicting state, append again, and assert ShouldHaveConstraintViolationFor(<Module>ConstraintNames.UniqueX) — the constraint name, never the message. Use per-test unique values ($"{Guid.NewGuid():N}") so specs aren't order-dependent. For a release, append the removal event then assert the value can be re-claimed.
ShouldNotBeSuccessful() and ShouldHaveValidationErrors() (or ShouldHaveConstraintViolationFor(name) for constraints). See the write-specs / write-specs-events skills.