5

I've implemented a rule engine like this:

public interface IRuleEngine
{
    List<ValidationMessage> Validate(param1, param2, param3);
}


public class DefaultRuleEngine : IRuleEngine
{
    readonly IList<IRule> _rules;

    public DefaultRuleEngine()
    {

        _rules = new List<IRule>()
        {
            new Rule1(),
            new Rule2(),
            new Rule3(),
            ....
        };
    }

    public List<ValidationMessage> Validate(param1, param2, param3)
    {
        var validationMessageList = new List<ValidationMessage>();

        foreach(var rule in _rules)
        {
            validationMessageList.Add(rule.Validate(param1, param2, param3));
        }

        return validationMessageList;
    }
}

    public interface IRule
{
    ValidationMessage Validate(param1, param2, param3);
}

public class Rule1 : IRule
{
    public ValidationMessage Validate(param1, param2, param3)
    {
        //do logic, return validation message

        return validationMessage;         
    }
}

Now, this works fine, but maintence has been tedious. I use the rule engine in many places in my code. I've also had to change the parameters it takes in a few times, which has lead to a lot of tedious modifications in all of the different classes where I use it.

I'm starting to think this design isn't that great, and what I can do to change it. What I've been thinking, is instead of passing in each individual param, I create a ValidationParameters class, and just pass that in. This way, when I need to add another parameter - param4 - I don't have to go to each place in the code to made the modification, and can just change the field in the ValidationParameters class. This I don't see being a problem, as not every rule uses each param, so I don't see running into any issues. Like, Rule1 may only use param2 and param3, not param1, for example.

public class ValidationParameters
{
    public string Param1 {get;set;}
    public string Param2 {get;set;}
    public int Param3 {get;set;}
    public int Param4 {get;set;}
}

and change the interface to

    public interface IRuleEngine
{
    List<ValidationMessage> Validate(ValidationParameters);
}

does this look like a good design, or is there a better pattern to use?

  • 3
    I built a rules engine a year or so ago that accepted Linq expressions for business rules. You can run a Visitor over the resulting expression tree, and it will provide a complete logging sequence when you execute it, which you can use for diagnostic or troubleshooting purposes. Have a look at Joseph Albahari's Predicate Builder as a starting point. See here for the Visitor implementation. – Robert Harvey Oct 12 '17 at 18:38
  • Have you looked at FluentValidation before? – Justin Oct 12 '17 at 19:55
  • ideally, i want to change the least amount of stuff to get this to work. So, is my solution terrible or is it workable? It seems to do what I need. If the other solutions are way better, I can look into changing it. – ygetarts Oct 12 '17 at 20:14
  • At the moment, your problem seems to be limits to the amount and type of parameters you can pass to your rules. I avoid this problem in my rules engine by allowing only one or two generic parameters in each rule, and supporting additional parameters by chaining rules together with AND and OR. – Robert Harvey Oct 12 '17 at 20:18
  • 5
    "Now, this works fine, but maintence has been tedious." - Welcome to the wonderful world of misplaced abstractions and procrastination. – whatsisname Oct 12 '17 at 22:55
3

And now I read the post's date....

Your design looks really clunky in my opinion. I think you should use an existing framework, a lot of work goes into doing this correctly. I for one do not want to see another rule engine written, as we say in Romanian "over the knee" (quickly, in an ad-hoc manner, without much attention to detail).

I just whipped this out in 20-30 minutes. By no means is this production grade, but if you really really really refuse to use a framework and want to start somewhere, I believe this abomination can help you. It should provide more flexibility.

class Employee
{
    public string Name { get; set; }
    public string Address { get; set; }
    public int BadgeNumber { get; set; }
    public decimal salary { get; set; }
    public int age { get; set; }
}

class ValidationResult
{
    public bool Valid { get; private set;}
    public string Message { get; private set; }

    private ValidationResult(bool success, string message = null)
    {

    }

    public static ValidationResult Success()
    {
        return new ValidationResult(true);
    }

    public static ValidationResult Fail()
    {
        return new ValidationResult(true);
    }

    public ValidationResult WithMessage(string message)
    {
        return new ValidationResult(this.Valid, message);
    }
}

class ValidationContext
{
    //might want to make this "freezable"/"popsicle" and perhaps
    //you might want to validate cross-entity somehow
    //will you always make a new entity containing 2 or 3 sub entities for this case?
    List<Rule> rules = new List<Rule>();

    public ValidationContext(params Rule[] rules)
    {
        this.rules = rules.ToList();
    }

    //how do you know each rule applies to which entity?
    private List<ValidationResult> GetResults()
    {
        var results = new List<ValidationResult>();
        foreach (var rule in rules)
        {
            results.Add(rule.Validate(this));
        }

        return results;
    }

    public void AddRule(Rule r)
    {
        this.rules.Add(r);
    }

}

abstract class Rule
{        
    public abstract ValidationResult Validate(ValidationContext context);

    public static Rule<TEntityType> Create<TEntityType>(TEntityType entity, Func<TEntityType, ValidationResult> rule)
    {
        Rule<TEntityType> newRule = new Rule<TEntityType>();
        newRule.rule = rule;
        newRule.entity = entity;
        return newRule;
    }
}

class Rule<T> : Rule
{
    internal T entity;
    internal Func<T, ValidationResult> rule;

    public override ValidationResult Validate(ValidationContext context)
    {
        return rule(entity);
    }
}

//usage: only rule creation since I need sleep. I have to hold an interview in 4 hours, I hope you are happy :)
class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee { age = 80 };
        var employeeMustBe80 = Rule.Create(employee,
                                           e => e.age > 80 ?
                                           ValidationResult.Success().WithMessage("he should retire") :
                                           ValidationResult.Fail().WithMessage("this guy gets no pension");
    }
}
  • Ty for the edit @Mael! – Alexandru Clonțea Mar 6 '18 at 6:57
  • do you have a suggested framework? – ygetarts Mar 6 '18 at 14:48
  • 1
    @ygetarts: Did you investigate Predicate Builder and the Visitor as I suggested in my comment below your question? We used this approach successfully in a production system. – Robert Harvey Mar 6 '18 at 16:03
  • 1
    @RobertHarvey M. Fowler seems to agree with you martinfowler.com/bliki/RulesEngine.html :) I too prefer more control, but sometimes I would use simple libraries if they are simple enough to understand fully. I have coded my fair share of rule engines (dynamic and static), that's why I know it is not necessarily trivial to the "untrained". The key point: No framework will provide good abstractions! I think what comes first is making sure the domain model or business layer or however you call it is efficient in solving the problems you need to solve. – Alexandru Clonțea Mar 6 '18 at 16:14
  • 1
    Predicate Builder is about 18 lines of code. We added some additional overloads to it to support two generic types instead of just one, and a trivial CreateRule helper method that allows you to declare your result variable using var instead of Expression<T, K, result>. It took awhile to work out the Visitor logger (you have to understand expression trees to write one), but it's still only about 20 lines of code, and you can treat it like a black box. – Robert Harvey Mar 6 '18 at 16:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.