NCommons Rules Engine
I recently decided to invest some time in learning how my team might leverage the MvcContrib rules engine for our projects at work. I discovered this feature after browsing the CodeCampServer source which seems to be the only publicly available example of the rules engine in use. I was impressed at how clean the controller actions were as a result of leveraging this feature in conjunction with some additional infrastructure sugar the CodeCampServer adds.
While I liked the capabilities of the MvcContrib rules engine overall, there were a few aspects I wanted to change. One of those things was the coupling to the validation strategy provided by the rules engine and another was the need for a bit of additional infrastructure code to help parse the results when mapping validation errors back to their corresponding UI elements.
I also recently became aware of the Fluent Validation library by Jeremy Skinner and thought to myself: “I wonder how long it would take to just create my own rules engine leveraging the Fluent Validation framework”, so I sat down last weekend to find out. Well, after getting something up and going, I thought I might as well share my results. I should note that given my effort was inspired by the MvcContrib rules engine, some of the same concepts are reflected in my effort (though perhaps with a more naive implementation).
Overview
The basic rules engine works as follows:
- A user invokes some Controller action.
- The Controller takes the action’s parameter and invokes the RulesEngine.Process() method.
- The RulesEngine invokes an injected implementation of IRuleValidator.Validate() on the object.
- The IRulesValidator returns a RuleValidationResult denoting the status of the validation as well as containing any validation error messages.
- The RulesEngine uses the Common Service Locator to find implementations of ICommand
where T matches the type of the object. - The implementations of ICommand
are executed and any results accumulated. - The RulesEngine returns a ProcessResult which contains the process status and any validation messages and return items.
- The Controller uses the status to determine which action to display. In the event of a validation failure, the error messages are added to the ModelState with their associated property names.
Here is an example usage:
[HttpPost] public ActionResult Create(ProductInput productInput) { ProcessResult results = _rulesEngine.Process(productInput); if (!results.Successful) { CopyValidationErrors(results); return View(productInput); } return RedirectToAction("Index"); } void CopyValidationErrors(ProcessResult results) { foreach (RuleValidationFailure failure in results.ValidationFailures) { ModelState.AddModelError(failure.PropertyName, failure.Message); } }
In an example application I’ve included with the source, I created an implementation of the IRulesValidator which adapts to the Fluent Validation library:
public class FluentValidationRulesValidator : IRulesValidator { public RuleValidationResult Validate(object message) { var result = new RuleValidationResult(); Type validatorType = typeof (AbstractValidator<>).MakeGenericType(message.GetType()); var validator = (IValidator) ServiceLocator.Current.GetInstance(validatorType); ValidationResult validationResult = validator.Validate(message); if (!validationResult.IsValid) { foreach (ValidationFailure error in validationResult.Errors) { var failure = new RuleValidationFailure(error.ErrorMessage, error.PropertyName); result.AddValidationFailure(failure); } } return result; } }
This uses the Common Service Locator to find types closing the Fluent Validation AbstractValidator
Mapping
I also included the ability to map UI types to domain types using a library such as AutoMapper. To express this as an optional feature, I created a MappingRulesEngine which has a dependency on an IMessageMapper. At first I went back and forth between expressing an IMessageMapper as an optional dependency to the RulesEngine, but I don’t really like property injection and the notion of using constructor injection for optional dependencies was a bit distasteful, so using inheritance felt like the cleanest and most expressive option.
The MappingRulesEngine uses the Common Service Locator to find a type closing AssociationConfiguration
public class ProductInputAssociationConfiguration : AssociationConfiguration<ProductInput> { public ProductInputAssociationConfiguration() { ConfigureAssociationsFor<Product>(x => { x.For(output => output.Id).Use(input => input.Id); x.For(output => output.Description).Use(input => input.Description); x.For(output => output.Price).Use(input => input.Price); }); } }
That’s about it. You can get the source at https://github.com/derekgreer/ncommons.