Entity validation engine using c# expression rules
Validating entity data is the common task when building business applications. Very often happens that we want to separate the rules from the app code to be able to quickly change it without affecting the application. In this article we will build simple validation engine that will be evaluating rules stored as linq expressions.
In our example we will hardcode the rules in separate .cs files (based on entity type). You may also want to store the linq expressions as a string in database and also compile the code dynamically from the string and then execute the rules based on the output dll file.
In our case we will hard code the rule in .css files. The following example shows the rule definitions for Client entity.
public class ClientValidationRules : BaseValidationRuleSet<Client> { public ClientValidationRules(Client arg) { RuleList.Add(new ValidationRule<Client>( //set the rule a => a.Name.Trim().Any() && !a.Name.Any(b => char.IsLower(b)) //// , arg) { //set the result Message = "Name cannot be all in uppercase!", ResultTypeIfFailed = ValidationResultType.ERROR, SuggestionString = arg.Name.ToLower() }); RuleList.Add(new ValidationRule<Client>( //set the rule a => a.DateOfBirth.AddYears(18) > DateTime.Now //// , arg) { //set the result Message = "You must be at lest 18 years old to register!", ResultTypeIfFailed = ValidationResultType.ERROR, SuggestionString = string.Format("Wait {0} days", DateTime.Now.Subtract(arg.DateOfBirth).TotalDays.ToString("0")) }); } } }
You can see that this way allows us to easily define rule’s logic based on the entity data. We can also define suggestion message for each rule separately. By setting ValidationResultType as a rule result we can for example allow to save data if this is only a warning or prevent saving the data by the user if this is an error.
The implementation is as follows. Let’s start with generic ValidationRule class containing the rule data and delegate to run.
namespace RuleEngine { public class ValidationRule<T> where T : class { public string Message { get; set; } public string SuggestionString { get; set; } public ValidationResultType ResultTypeIfFailed { get; set; } internal Func<T, bool> RuleDelegate { get; set; } internal T ObjectTovalidate { get; set; } public ValidationRule(Func<T, bool> rule, T arg) { RuleDelegate = rule; ObjectTovalidate = arg; } public bool RunRuleDelegate() { return RuleDelegate(ObjectTovalidate); } } }
It’s base class will have the “Run” method implementation.
namespace RuleEngine { public abstract class BaseValidationRuleSet<T> where T : class { public List<ValidationRule<T>> RuleList { get; internal set; } /// <summary> /// Initiates rule list /// </summary> public BaseValidationRuleSet() { RuleList = new List<ValidationRule<T>>(); } /// <summary> /// Run all added rules /// </summary> /// <returns></returns> public ValidationResult Run() { foreach (var rule in this.RuleList) { var result = rule.RunRuleDelegate(); if (result) { return new ValidationResult() { IsValid = false, ResultType = rule.ResultTypeIfFailed, Message = rule.Message, SuggestionString = rule.SuggestionString }; } } return new ValidationResult() { IsValid = true, ResultType = ValidationResultType.OK }; } } }
Let’s define ValidationResult class that will be returned after running the rule delegate.
namespace RuleEngine { public class ValidationResult { public bool IsValid { get; set; } public string SuggestionString { get; set; } public string Message { get; set; } public ValidationResultType ResultType { get; set; } } }
The ValidationEngine class will have the entry point for different entity validation methods. You may also want to create generic function to pass the validating object to. You also can preload all rules at runtime and evaluate dynamically using AppDomain.CurrentDomain.GetAssemblies() method.
public class ValidationEngine: ValidationEngineBase { public ValidationResult RunProjectRules(Project project) { return new ProjectValidationRules(project).Run(); } public ValidationResult RunClientRules(Client client) { return new ClientValidationRules(client).Run(); } }
In order to test our solution we can use console app. We also need to load some fake data into our entities.
static void Main(string[] args) { var validationEngine = new ValidationEngine(); #region load entities var client = new Client() { Name = "CLIENT NUMBER1a", DateOfBirth = DateTime.Now.AddYears(-10) }; var project = new Project() { Name = "PROJECT NUMBER1", StartDate = DateTime.Now.AddDays(1), EndDate = DateTime.Now.AddDays(1) }; #endregion #region validate client Console.WriteLine("Starting client validation"); var result = validationEngine.RunClientRules(client); if (!result.IsValid) { Console.WriteLine(result.Message); Console.WriteLine(string.Format("Suggestion: {0}", result.SuggestionString)); } else { Console.WriteLine("OK"); } #endregion Console.WriteLine("--------------------"); #region validate project Console.WriteLine("Starting project validation"); result = validationEngine.RunProjectRules(project); if (!result.IsValid) { Console.WriteLine(result.Message); Console.WriteLine(string.Format("Suggestion: {0}", result.SuggestionString)); } else { Console.WriteLine("OK"); } #endregion Console.ReadLine(); }
I have included project files bellow for your tests.
#Article update#
We can also create simple business rule engine in very similar way, here is the implementation:
namespace RuleEngine { /// <summary> /// To be extended if needed /// </summary> public abstract class BusinessRuleEngineBase { } /// <summary> /// Runs set of rules for different entities /// </summary> public class BusinessRuleEngine : BusinessRuleEngineBase { /// <summary> /// Run rules for ScheduledFlight /// </summary> /// <param name="scheduledFlight"></param> /// <returns></returns> public bool RunBusinessRulesFor(ScheduledFlight scheduledFlight) { return new FlightBusinessRules(scheduledFlight).Run(); } } }
Business rule implementation containing evaluating and executing delegate
/// <summary> /// Runs delegate on the specified business rule set /// </summary> /// <typeparam name="T"></typeparam> public class BusinessRule<T> where T : class { public string RuleName { get; set; } internal Func<T, bool> RuleDelegate { get; set; } internal Func<T, bool> ExecuteRuleDelegate { get; set; } internal T ObjectToValidate { get; set; } internal T ObjectToApply { get; set; } public BusinessRule(Func<T, bool> rule, Func<T, bool> ruleApply, T arg) { RuleDelegate = rule; ExecuteRuleDelegate = ruleApply; ObjectToValidate = arg; ObjectToApply = arg; } /// <summary> /// Determines if business rule should be applied /// </summary> /// <returns></returns> public bool ApplyRuleDelegate() { return RuleDelegate(ObjectToValidate); } /// <summary> /// Applies business rule on the current entity /// </summary> /// <returns></returns> public bool ApplyBusinessDelegate() { return ExecuteRuleDelegate(ObjectToApply); } }
BaseBusinessRuleSet base class
/// <summary> /// Base class for business rule sets /// </summary> /// <typeparam name="T"></typeparam> public abstract class BaseBusinessRuleSet<T> where T : class { public List<BusinessRule<T>> RuleList { get; internal set; } /// <summary> /// Initiates business rule list /// </summary> public BaseBusinessRuleSet() { RuleList = new List<BusinessRule<T>>(); } /// <summary> /// Run all added business rules /// </summary> /// <returns></returns> public bool Run() { foreach (var rule in this.RuleList) { var result = rule.ApplyRuleDelegate(); //if rule is true, run business logic if (result) { rule.ApplyBusinessDelegate(); return true; } } return false; } }
And finally the business rule set-up class.
public class FlightBusinessRules : BaseBusinessRuleSet<ScheduledFlight> { /// <summary> /// Setup business rules for ScheduledFlight /// </summary> /// <param name="arg"></param> public FlightBusinessRules(ScheduledFlight arg) { RuleList.Add(new BusinessRule<ScheduledFlight>( //set the rule a => (double)arg.Passengers.Count(p => p.Type == PassengerType.AirlineEmployee) / arg.Passengers.Count > arg.FlightRoute.MinimumTakeOffPercentage , //execute rule e => { //lower the base price till 100 if (arg.FlightRoute.BasePrice >= 100) { arg.FlightRoute.BasePrice -= 10; } //add more logic here... return true; }, arg) { RuleName = "ApplyDiscountFor_AirlineEmployee" }); RuleList.Add(new BusinessRule<ScheduledFlight>( //set the rule a => a.FlightRoute.BasePrice == 0 , //execute rule e => { //enforce minimal base price arg.FlightRoute.BasePrice = 100; return true; }, arg) { RuleName = "EnsureMinimalBasePrice" }); } }