Have you ever found yourself debugging a validation issue, only to discover the logic is scattered across controllers, services, and models? Or copying the same validation code between projects?
There's a better way.
Introducing Masterly.BusinessRules
Masterly.BusinessRules is a clean, composable, and extensible business rule engine for .NET that brings order to your validation chaos.
dotnet add package Masterly.BusinessRules
The Problem
Business rules are everywhere in your code:
// In your controller
if (order.Items.Count == 0)
return BadRequest("Order must have items");
// In your service
if (!customer.IsActive)
throw new Exception("Customer must be active");
// In another service
if (order.Total > customer.CreditLimit)
throw new Exception("Order exceeds credit limit");
This leads to:
- Duplicated validation logic
- Inconsistent error messages
- Hard-to-test business rules
- Scattered domain knowledge
The Solution
Encapsulate each business rule in its own class:
public class OrderMustHaveItemsRule(Order order) : BaseBusinessRule
{
public override string Code => "ORDER.NO_ITEMS";
public override string Message => "Order must contain at least one item.";
public override bool IsBroken() => !order.Items.Any();
}
Then use it anywhere:
new OrderMustHaveItemsRule(order).Check(); // Throws if broken
Why Developers Love It
1. Fluent Builders for Quick Rules
Don't want to create a class? Use the builder:
var rule = BusinessRuleBuilder.Create("AGE.INVALID")
.WithMessage("Must be 18 or older")
.WithSeverity(RuleSeverity.Error)
.WithCategory("User Validation")
.When(() => user.Age < 18)
.Build();
2. Compose Rules Like LEGO Blocks
Combine simple rules into complex validation logic:
var accessRule = new CustomerIsAdultRule(customer)
.And(new CustomerIsActiveRule(customer))
.Or(new CustomerIsVIPRule(customer));
// Customer must be (adult AND active) OR VIP
3. First-Class Async Support
Database queries? API calls? No problem:
public class EmailMustBeUniqueRule(IUserRepository repo, string email)
: BaseAsyncBusinessRule
{
public override string Code => "EMAIL.DUPLICATE";
public override string Message => $"Email '{email}' is already registered.";
public override async Task<bool> IsBrokenAsync(
BusinessRuleContext context,
CancellationToken ct = default)
=> await repo.EmailExistsAsync(email, ct);
}
4. Batch Validation with Control
Check multiple rules at once with options:
// Fail fast - stop on first broken rule
BusinessRuleChecker.CheckAll(
new OrderMustHaveItemsRule(order),
new CustomerMustBeActiveRule(customer),
stopOnFirstFailure: true
);
// Run async rules in parallel for performance
await AsyncBusinessRuleChecker.CheckAllAsync(
context,
rules,
runInParallel: true
);
5. API-Friendly Error Collection
Get all validation errors without exceptions:
var result = await AsyncBusinessRuleChecker.EvaluateAllAsync(rules);
if (result.HasBrokenRules)
{
return BadRequest(new {
errors = result.BrokenRules.Select(r => new {
code = r.Code,
message = r.Message,
severity = r.Severity
})
});
}
6. Built-in Caching for Expensive Rules
Cache results for rules that hit the database:
var cachedRule = new CachedBusinessRule(
new ExpensiveValidationRule(),
TimeSpan.FromMinutes(5)
);
// Or use extension method
var cachedRule = myRule.WithCache(TimeSpan.FromMinutes(5));
7. Conditional Execution
Run rules only when conditions are met:
var premiumRule = new ConditionalBusinessRule(
new PremiumFeatureValidation(),
() => user.IsPremium // Only check for premium users
);
8. Rich Metadata for Organization
Categorize and filter your rules:
public class OrderLimitRule : BaseBusinessRule
{
public override string Code => "ORDER.LIMIT";
public override string Message => "Order exceeds daily limit.";
public override string Name => "Daily Order Limit";
public override string Description => "Validates orders don't exceed daily spending limits";
public override string Category => "Financial";
public override IEnumerable<string> Tags => ["order", "limit", "financial"];
public override RuleSeverity Severity => RuleSeverity.Error;
public override bool IsBroken() => /* validation logic */;
}
// Filter by category or tags
BusinessRuleChecker.CheckByCategory(context, "Financial", rules);
BusinessRuleChecker.CheckByTags(context, ["order"], rules);
9. Observability Built-in
Monitor rule execution for logging and metrics:
public class LoggingObserver : IRuleExecutionObserver
{
public void OnBeforeEvaluate(IBusinessRule rule, BusinessRuleContext context)
=> _logger.LogDebug("Evaluating rule: {Code}", rule.Code);
public void OnRuleBroken(IBusinessRule rule, BusinessRuleContext context)
=> _logger.LogWarning("Rule broken: {Code} - {Message}", rule.Code, rule.Message);
}
BusinessRuleChecker.CheckAll(context, observer, rules);
10. Testing Made Easy
Dedicated testing utilities:
[Fact]
public void OrderWithNoItems_ShouldBeBroken()
{
var order = new Order { Items = [] };
var rule = new OrderMustHaveItemsRule(order);
RuleTestHelper.AssertBroken(rule);
}
[Fact]
public async Task DuplicateEmail_ShouldBeBroken()
{
var rule = new EmailMustBeUniqueRule(mockRepo, "taken@email.com");
await RuleTestHelper.AssertBrokenAsync(rule);
}
Real-World Example: Order Processing
public class OrderService(IRepository repository)
{
public async Task<Result> ProcessOrderAsync(Order order, Customer customer)
{
var context = new BusinessRuleContext();
context.Set("orderId", order.Id);
var rules = new IAsyncBusinessRule[]
{
new OrderMustHaveItemsRule(order).ToAsync(),
new CustomerMustBeActiveRule(customer).ToAsync(),
new OrderWithinCreditLimitRule(order, customer).ToAsync(),
new InventoryAvailableRule(repository, order)
};
var result = await AsyncBusinessRuleChecker.EvaluateAllAsync(
context,
rules,
runInParallel: true
);
if (result.HasBrokenRules)
{
return Result.Failure(result.BrokenRules
.Select(r => $"[{r.Code}] {r.Message}"));
}
// Process the order...
return Result.Success();
}
}
Key Features at a Glance
| Feature | Description |
|---|---|
| Sync & Async | Full support for both patterns with CancellationToken |
| Fluent Builders | Create rules inline without subclassing |
| Composition |
And(), Or(), Not() operators |
| Context | Pass data between rules with typed/untyped context |
| Caching | Cache expensive rule evaluations |
| Conditional | Execute rules based on preconditions |
| Batch Checking | Fail-fast, parallel execution, filtering |
| Evaluation Results | Non-throwing API for collecting all errors |
| Observers | Logging, metrics, audit trail hooks |
| Testing Utilities | AssertBroken, AssertNotBroken helpers |
Get Started
dotnet add package Masterly.BusinessRules
Check out the full documentation and examples on GitHub.
Conclusion
Stop scattering your business logic. With Masterly.BusinessRules, you get:
- Clean separation of business rules from application code
- Reusable validation logic across your entire application
- Testable rules with dedicated testing utilities
- Composable rules that combine like building blocks
- Observable execution for logging and monitoring
- Performant validation with caching and parallel execution
Your future self (and your team) will thank you.
What business rules are you validating today? Drop a comment below!
Top comments (0)