kgrzybek/modular-monolith-with-ddd · error · BusinessRuleValidationException
{rule.Message}
Error message
{rule.Message} What it means
Thrown by AggregateRoot.CheckRule whenever an IBusinessRule's IsBroken() returns true. This is the central domain-invariant enforcement hook: aggregates call CheckRule with a rule object, and if the rule is broken a BusinessRuleValidationException is thrown carrying the rule and its Message. The actual message comes from the specific rule class (e.g. 'Member cannot comment on a meeting they are not part of'), so {rule.Message} is a template resolved at throw time.
Source
Thrown at src/Modules/Payments/Domain/SeedWork/AggregateRoot.cs:42
Version = -1;
}
public void Load(IEnumerable<IDomainEvent> history)
{
foreach (var e in history)
{
Apply(e);
Version++;
}
}
protected abstract void Apply(IDomainEvent @event);
protected static void CheckRule(IBusinessRule rule)
{
if (rule.IsBroken())
{
throw new BusinessRuleValidationException(rule);
}
}
}
}View on GitHub (pinned to 91c8ef24b4)
Solutions
- Read ex.BrokenRule.GetType().Name / ex.Details to identify which invariant was violated and address that specific condition.
- Ensure the aggregate is in the correct state before invoking the operation (e.g. meeting commenting enabled, member is a group member, item active).
- Handle concurrency by reloading the aggregate and retrying once after the conflict resolves.
- Surface the rule Message to the user as an actionable validation error (the API already maps this via BusinessRuleValidationExceptionProblemDetails).
Example fix
// before
subscription.Renew(payment);
// after
try
{
subscription.Renew(payment);
}
catch (BusinessRuleValidationException ex)
{
// ex.Details / ex.BrokenRule.Message describes the violated invariant
return BadRequest(new { error = ex.Details });
} Defensive patterns
Strategy: try-catch
Try / catch
try
{
aggregate.DoOperation(...);
}
catch (BusinessRuleValidationException ex)
{
// ex.BrokenRule is the IBusinessRule; ex.Details == ex.BrokenRule.Message
return BadRequest(new { rule = ex.BrokenRule.GetType().Name, message = ex.Details });
} Prevention
- Read BrokenRule.GetType().Name first; it names the exact invariant violated.
- Put the aggregate into the required state before invoking the operation.
- For concurrency, reload the aggregate and retry once after resolving the conflict.
- Surface rule.Message to the user as an actionable validation message (API already maps it via BusinessRuleValidationExceptionProblemDetails).
When it happens
Trigger: Any aggregate operation that violates a business invariant: liking twice, commenting when commenting is disabled, editing another member's comment, renewing with a mismatched price, changing an inactive item, etc. The exact scenario is encoded in the rule class whose Message appears in Details.
Common situations: Client attempts an action the current state does not permit (concurrent edits, stale permissions, wrong lifecycle state); race conditions where two commands pass pre-checks but the second violates the invariant; tests that skip state setup.
AI-assisted analysis of kgrzybek/modular-monolith-with-ddd@91c8ef24b4 (2026-08-13).
Data as JSON: /api/errors/2bc423146b72589b.
Report an issue: GitHub.