DEV Community

Cover image for Stop Scattering Your Business Logic: Meet Masterly.BusinessRules for .NET
Ahmad Al-Freihat
Ahmad Al-Freihat

Posted on

Stop Scattering Your Business Logic: Meet Masterly.BusinessRules for .NET

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Then use it anywhere:

new OrderMustHaveItemsRule(order).Check(); // Throws if broken
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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
        })
    });
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

7. Conditional Execution

Run rules only when conditions are met:

var premiumRule = new ConditionalBusinessRule(
    new PremiumFeatureValidation(),
    () => user.IsPremium  // Only check for premium users
);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!


Star on GitHub

Top comments (0)