Windows Workflow foundation has a built-in dialog for editing business rules. The RuleSetDialog class gives you everything you need to edit business rules, providing Intellisense based on the type of entity you provided to the RuleSetDialog constructor. Assume we have a business entity called Customer, and we want to create a new set of business rules for it. We would instantiate the dialog like this:
Type entityType = typeof(Customer);
RuleSet ruleSet = new RuleSet();
dialog = new RuleSetDialog(entityType, null, ruleSet);
DialogResult result = dialog.ShowDialog(this);
The null is for an optional TypeProvider which is beyond the scope of this article, and isn’t used here. The text boxes in the dialog have full Intellisense showing the properties and methods of the Customer class. The Intellisense isn’t exactly as robust as that in Visual Studio, but it’s better than nothing. From here you can write your If/Then/Else logic, set properties based on other properties, call methods when certain conditions are found, etc.
Since we’re using the WF Rules engine outside of an actual WF Workflow, the loading and saving of the rules is left up to you. There are several examples out there, and the workflow code samples include examples of doing this. Rules can be persisted to .rules files, or to a database via code. The important thing to know here is that the resulting RuleSet doesn’t have any idea what kind of object it was written for. The WF Runtime would presumably take care of this on its own, but since we’re hosting the Rules ourselves, it’s up to us to remember this. The RuleSet itself is basically just a representation of the rules themselves.
Serializing/Deserializing RuleSets
Loading and saving RuleSets from the database isn’t that difficult. It’s just like any other business entity in a typical system, except that it’s not really a single object, but a pretty deep hierarchy representing an expression tree. You could spend weeks designing a database structure to hold the component parts, but fortunately you don’t have to. You can use the WorkflowMarkupSerializer class to turn the whole thing into one big lump of XML, and save that to a nvarchar(max) field in your database. It saves a lot of headaches, and doesn’t take much code to accomplish.
Where I’ve had to host the WF Rules engine myself on client projects, I have created a BusinessRuleSet entity, and given it fields to store the XAML representation of the rules, as well as what classes they apply to, along with whatever auditing fields (CreatedBy, ModifiedDate, etc) are required by project and/or client standards. At runtime, you load the appropriate BusinessRuleSet entity back up, extract the RuleSet, and execute it against an instance of the business entity.
Note: I have called the entity "BusinessRuleSet", and not "RuleSet" in order to avoid confusing naming conflicts with WF’s RuleSet class.
After closing the RuleSetDialog you can call its RuleSet property to get the RuleSet that was being edited. Using the WorkflowMarkupSerializer’s Serialize method, we’ll turn it into a XAML string ready for saving. Here’s a helper method that encapsulates the whole process, and a matching deserialization method to use when loading the rules back from the database.
private string SerializeRuleSet(RuleSet ruleSet)
{
StringBuilder ruleSetXml = new StringBuilder();
using (XmlWriter xmlWriter = XmlWriter.Create(ruleSetXml))
{
new WorkflowMarkupSerializer().Serialize(xmlWriter, ruleSet);
}
return ruleSetXml.ToString();
}
private RuleSet DeserializeRuleSet(string ruleSetXmlDefinition)
{
RuleSet result = null;
if (!String.IsNullOrEmpty(ruleSetXmlDefinition))
{
using (XmlReader reader = XmlReader.Create
(new StringReader(ruleSetXmlDefinition)))
{
result = serializer.Deserialize(reader) as RuleSet;
}
}
return result;
}
Ensuring RuleSets are valid for the instances they run against
So, if the rules don’t know about their target entity types, what’s to stop you from loading the Customer RuleSet and applying it against an Order instance? Verifying that a RuleSet can be applied to a specific entity is the responsibility of the RuleValidation class. The constructor for the RuleValidation class takes the type of item you want to run rules against, but oddly enough, doesn’t take the rules themselves. The RuleValidation instance is then passed to a RuleExecution class, which links it up to an actual instance of the class you want to validate. Finally, you call the Execute method on the RuleSet, passing in the RuleExecution instance. There are a lot of moving parts here, but in the end it looks pretty much like this:
RuleValidation validation = new RuleValidation(entity.GetType(), null);
RuleExecution execution = new RuleExecution(validation, entity);
RuleSet ruleSet = DeserializeRuleSet(businessRuleSet.RuleSet);
ruleSet.Execute(execution);
The RuleValidation will be used during execution to verify that every property or method mentioned in the rule is actually available on the entity we’re executing against, and to throw an exception if not. This may seem a little obtuse, and over-complicated, but it’s going to allow us do something pretty cool.
Limiting the surface area visible to the RuleSet designer
When you first start using the RuleSetDialog to edit rules, you will probably notice that the Intellisense in the If/Then/Else textboxes will show every member of the type you are writing a rule for. Visibility doesn’t seem to matter, and every private and protected member of the class becomes available to write rules against. Since the idea of hosting the WF Rules engine inside the app in the first place is so that we can allow business analysts and other non-programmer staff to modify and define business rules, we probably don’t want to open everything up quite that wide, and would prefer to expose a more limited subset of properties and methods.
The seemingly over-complicated series of objects required to execute a RuleSet in the previous section will now work to our advantage because t makes it possible to "lie" to the dialog about what Type we’ll eventually be executing the RuleSet against. We could actually write a RuleSet against the Customer type and execute it against the Employee type, so long as the RuleSet only touched properties that those two classes have in common. We won’t be doing anything quite that drastic, but the basic idea will form the basis for restricting what properties of an object are visible to the dialog.
We can create a rules-specific interface definition that only includes those items we want the rules engine to have access to. We can then implement this interface on the actual Business Entity class without having to do any extra work because all the members of the interface should already be present on the entity class. This is a kind of over-simplified Façade, for lack of a better term.
Implementing the rules interface
We’ll start with a base interface which exposes common functionality that all business entities can be expected to have, such as ID properties, audit fields, etc. It will also serve as a "marker" interface which will allow us to find the rules interface more easily later on. Here is a simplistic case which illustrates the point:
public interface IRulesEntity
{
decimal Id { get; }
}
interface ICustomer : IRulesEntity
{
string FirstName { get; set; }
string LastName { get; set; }
string Address1 { get; set; }
string Address2 { get; set; }
}
Showing the RuleSetDialog
Now, when it comes time to instantiate the RuleSetDialog, we’ll just pass it the ICustomer interface instead of the actual Customer class. The dialog’s Intellisense will now only offer those properties and methods that are available through the ICustomer interface. To do this we’ll need a way to find the rules interface for a given entity class at runtime, and we’ll use some reflection and Linq functionality to do this.
Remember that BusinessRuleSet is our custom class that stores the RuleSet XAML and the full name of the type it applies to. With this information, we can find the class, and its rules interface if it has one.
public void ShowDialog(BusinessRuleSet businessRuleSet)
{
string activityName = businessRuleSet.ActivityName;
Type baseType = typeof(IRulesEntity);
Type entityType = baseType.Assembly.GetType(activityName);
Type interfaceType = EntityType.GetInterfaces()
.Where(t => (t != baseType) && (baseType.IsAssignableFrom(t)))
.SingleOrDefault();
RuleSet ruleSet = DeserializeRuleSet(businessRuleSet.RuleSet);
dialog = new RuleSetDialog(entityType, null, ruleSet);
DialogResult result = dialog.ShowDialog(this);
}
Of course, we want to make sure that we gracefully handle the case where we’ve been asked to edit a BusinessRuleSet for a class that has no rules interface. For my current project, when adding a new RuleSet, the user must choose from a list of available classes which, of course, only contains classes which implement a rules interface, so this isn’t a problem. Finding the appropriate classes can be accomplished through a similar combination of reflection and Linq querying.
public List<Type> GetEntities()
{
Type baseType = typeof(IRulesEntity);
List<Type> types = new List<Type>();
baseType.Assembly.GetTypes()
.Where(t => t.IsClass && (baseType.IsAssignableFrom(t)))
.Each(entityType =>
{
Type interfaceType = entityType.GetInterfaces()
.Where(t => (t != baseType)
&& (baseType.IsAssignableFrom(t)))
.SingleOrDefault();
types.Add(entityType);
});
}
The call through SingleOrDefault will throw an error if a single class implements more than one Rules interface, which we’ve decided not to support in our current project. If you want to support multiple Rules interfaces, you can implement that yourself. The ".Each" syntax is an extension method against IEnumerable, which is also pretty cool. It looks like this:
public static void Each<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
Steve Harman has an explanation of the .Else extension here: http://stevenharman.net/blog/archive/2008/01/15/get-ruby-esq-each-iterators-in-c-3.0.aspx.Technorati Tags: C#,WF,Reflection,Extension Method
very good article and Nice support article.
Thank you for sharing information.