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

  1. Read ex.BrokenRule.GetType().Name / ex.Details to identify which invariant was violated and address that specific condition.
  2. Ensure the aggregate is in the correct state before invoking the operation (e.g. meeting commenting enabled, member is a group member, item active).
  3. Handle concurrency by reloading the aggregate and retrying once after the conflict resolves.
  4. 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

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.