{"record":{"id":"2bc423146b72589b","repo":"kgrzybek/modular-monolith-with-ddd","slug":"rule-message","errorCode":null,"errorMessage":"{rule.Message}","messagePattern":"\\{rule\\.Message\\}","errorType":"validation","errorClass":"BusinessRuleValidationException","httpStatus":409,"severity":"error","filePath":"src/Modules/Payments/Domain/SeedWork/AggregateRoot.cs","lineNumber":42,"sourceCode":"            Version = -1;\n        }\n\n        public void Load(IEnumerable<IDomainEvent> history)\n        {\n            foreach (var e in history)\n            {\n                Apply(e);\n                Version++;\n            }\n        }\n\n        protected abstract void Apply(IDomainEvent @event);\n\n        protected static void CheckRule(IBusinessRule rule)\n        {\n            if (rule.IsBroken())\n            {\n                throw new BusinessRuleValidationException(rule);\n            }\n        }\n    }\n}","sourceCodeStart":24,"sourceCodeEnd":46,"githubUrl":"https://github.com/kgrzybek/modular-monolith-with-ddd/blob/91c8ef24b4cb6ef558c95d8267fa07d68c7059f8/src/Modules/Payments/Domain/SeedWork/AggregateRoot.cs#L24-L46","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nsubscription.Renew(payment);\n\n// after\ntry\n{\n    subscription.Renew(payment);\n}\ncatch (BusinessRuleValidationException ex)\n{\n    // ex.Details / ex.BrokenRule.Message describes the violated invariant\n    return BadRequest(new { error = ex.Details });\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try\n{\n    aggregate.DoOperation(...);\n}\ncatch (BusinessRuleValidationException ex)\n{\n    // ex.BrokenRule is the IBusinessRule; ex.Details == ex.BrokenRule.Message\n    return BadRequest(new { rule = ex.BrokenRule.GetType().Name, message = ex.Details });\n}","preventionTips":["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)."],"tags":["domain","business-rule","ddd","aggregate-root","invariant","business-rule-validation-exception"],"backgroundTag":null,"analyzedSha":"91c8ef24b4cb6ef558c95d8267fa07d68c7059f8","analyzedAt":"2026-08-13T17:18:16.571Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}