Sunday, January 24, 2010

How to Track the RuleEngine rules in WF without starting a workflow runtime

Gents,

Recently I had a problem at hands, I wanted to run run the RuleEngine from Workflow Foundation outside WF.
It is possible and easy to run the RuleEngine outside of WF, but the possibility to track which rules were executed was skipped, as far as I found and saw in the API.

You can track executed rules when you run the workflow runtime itself and attach a TrackingService, but it came out to slow in the test we did.

The problem was that every time we called a WCF service method, the runtime was started for executing a couple of rules, sounds like overkill to me.
So I created a little ‘hack’ to execute the rules with tracking without actually starting the workflow.

Let’s jump into code :)

I created a RuleHelper<T> that executes my rules.
Below you can find the execute method of the helper class:

public void Execute(T objectToRunRulesOn, bool trackData)
{
if (trackData)
{
// Create dummy ActivityExecutionContext and see that trackData is intercepted.
// Initialize with own activity and insert IWorkflowCoreRuntime

Type activityExecutionContext = typeof(Activity).Assembly.GetType("System.Workflow.ComponentModel.ActivityExecutionContext");
var ctor = activityExecutionContext.GetConstructor(BindingFlags.Instance BindingFlags.NonPublic, null,
new[] { typeof(Activity) }, null);
var activity = new InterceptingActivity();
var context = ctor.Invoke(new object[] { activity });

_ruleEngine = new RuleEngine(_ruleSet, typeof(T));
lock (SyncRoot)
{
InterceptingActivity.Track += InterceptingActivity_Track;
_ruleEngine.Execute(objectToRunRulesOn, (ActivityExecutionContext) context);
InterceptingActivity.Track -= InterceptingActivity_Track;
}
}
else
{
Execute(objectToRunRulesOn);
}
}


The intercepting activity does actually all the work.

We first create our own (dummy) ActiviyExecutionContext, problem was, that it is an ‘internal’ class.

The argument to the context is our own InterceptingActivity.
After the instantiation we create our RuleEngine (from the WF assemblies) with a loaded or created RuleSet and the type where to run the RuleSet on.
Then some plumbing code for multiple threads (SyncRoot) and then we attach a static event to the activity.
Next we will execute the rules with the RuleEngine on the specified object that needs checking, and with the self created context which has knowledge of our own created Activity.

Let’s see what the InterceptingActivity does:

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Workflow.Activities.Rules;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.Runtime;

namespace Whizzo.Framework.Rules
{
public class InterceptingActivity : Activity
{
// Some caching variables (ExecutionEvent)
private static FieldInfo _argsFieldInfo;
// Some caching variables (InjectAllTheNeededHandlers)
private static ConstructorInfo _executorCtr;
private static PropertyInfo _currActivity;
private static Delegate _handlerDelegate;
private static FieldInfo _workflowExecutionEventFieldInfo;
private static FieldInfo _workflowCoreRuntimeFieldInfo;

// Static event for tracking rules
public static event EventHandler<TrackingEventArgs> Track;

public InterceptingActivity()
: base("InterceptingActivity")
{
InjectAllTheNeededHandlers();
}

private void InjectAllTheNeededHandlers()
{
if (_handlerDelegate == null)
{
// Get the type of the WorkflowExecutor
Type executorType =
typeof (WorkflowEventArgs).Assembly.GetType("System.Workflow.Runtime.WorkflowExecutor");
// Get eventargs type, the event and the handler type
Type eventTypeType =
typeof (WorkflowEventArgs).Assembly.GetType(
"System.Workflow.Runtime.WorkflowExecutor+WorkflowExecutionEventArgs");
EventInfo evt = executorType.GetEvent("WorkflowExecutionEvent",
BindingFlags.Instance BindingFlags.NonPublic);
Type handlerType = TypeProvider.GetEventHandlerType(evt);
// Get current activity of WorkflowExecutor
_currActivity = executorType.GetProperty("CurrentActivity",
BindingFlags.Instance BindingFlags.NonPublic);
// Get the constructor
_executorCtr = executorType.GetConstructor(BindingFlags.Instance BindingFlags.NonPublic, null,
new[] {typeof (Guid)}, null);

// Get field which has the event handler
_workflowExecutionEventFieldInfo = executorType.GetField("_workflowExecutionEvent",
BindingFlags.Instance BindingFlags.NonPublic);
// Get workflowCoreRuntime field of activity
_workflowCoreRuntimeFieldInfo = typeof (Activity).GetField("workflowCoreRuntime",
BindingFlags.Instance BindingFlags.NonPublic);

// Create dynamic method in module of workflow
Module m = typeof (WorkflowEventArgs).Assembly.GetModules()[0];
DynamicMethod dm = new DynamicMethod("MyHandler", null, new[] {typeof (object), eventTypeType}, m, true);
MethodInfo execMethod = GetType().GetMethod("ExecutionEvent");

// Generate method body
ILGenerator ilgen = dm.GetILGenerator();
ilgen.Emit(OpCodes.Nop);
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldarg_1);
ilgen.Emit(OpCodes.Call, execMethod);
ilgen.Emit(OpCodes.Nop);
ilgen.Emit(OpCodes.Ret);

// Create delegate
_handlerDelegate = dm.CreateDelegate(handlerType);
}

// Create instance of WorkflowExecutor
object executor = _executorCtr.Invoke(new object[] { Guid.NewGuid() });
// Set current activity of WorkflowExecutor
_currActivity.SetValue(executor, this, null);

// Attach delegate to event
_workflowExecutionEventFieldInfo.SetValue(executor, _handlerDelegate);

// Set executor as workflowCoreRuntime
_workflowCoreRuntimeFieldInfo.SetValue(this, executor);
}

public static void ExecutionEvent(object sender, EventArgs eventArgs)
{
if(Track != null)
{
if (_argsFieldInfo == null)
{
_argsFieldInfo = eventArgs.GetType().GetField("_args",
BindingFlags.NonPublic BindingFlags.Instance);
}
var argsValue = _argsFieldInfo.GetValue(eventArgs);
// Extract args
RuleActionTrackingEvent args = (RuleActionTrackingEvent) argsValue;
Track(sender, new TrackingEventArgs(args));
}
}
}
}

InjectAllTheNeededHandlers is the most important method,
it will create a System.Workflow.Runtime.WorkflowExecutor and it will initialize all it’s needed variables to be able to execute the rules.

It will also attach a self created DynamicMethod converted to a Delegate as an event handler in the WorkflowExecutor.

The event will be fired by the executor (internal class of Microsoft) and this way we can intercept the result of the executed rules.

The method that will get fired is ExecutionEvent, which will fire another event if attached (in the Execute method of the helper) and which will pass the RuleName and the Result of execution.

Voila, hopefully this was a little bit clear :)

There are probably other ways to do this (without starting the runtime) but this way was good enough for me :)

At a blog (http://blogs.msdn.com/moustafa/archive/2006/08/05/689776.aspx) I also found another method to track the executed rules outside the workflow,
But it involved Tracing while this method uses the tracking inside the workflow, without actually executing it. It's a lot simpler, and probably cleaner.

You will have to enable tracing for WF and write a custom TraceListener.

But I prefer to do it without diagnostics tracing put on.

Some feedback in the comments indicated that there were a couple of thing short in the code I displayed above. I'll try to complete it a little more.

This is the TrackingEventArgs class, used for triggering the 'Track' event, which we subscribe to, to track the rules.



using System;
using System.Workflow.Activities.Rules;

namespace Whizzo.Framework.Rules
{
public class TrackingEventArgs : EventArgs
{

public TrackingEventArgs(RuleActionTrackingEvent args, string rulesetName)
{
Args = args;
RulesetName = rulesetName;
}

public RuleActionTrackingEvent Args { get; private set; }

public string RulesetName { get; private set; }

}
}

This is the complete RuleHelper class, I will show below how to call it:


using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Workflow.Activities.Rules;
using System.Workflow.Activities.Rules.Design;
using System.Workflow.ComponentModel;
using System.Reflection;
using System.Collections.Generic;

namespace Whizzo.Framework.Rules
{
public class RuleHelper<T>
{
private RuleSet _ruleSet;
private RuleEngine _ruleEngine;
private static readonly object SyncRoot = new object();
private List<TrackingEventArgs> _ruleMessages;

public RuleHelper()
{
_ruleMessages = new List<TrackingEventArgs>();
}

public void SetRules(RuleSet ruleSet)
{
_ruleSet = ruleSet;
}

public void Execute(T objectToRunRulesOn)
{
_ruleEngine = new RuleEngine(_ruleSet, typeof(T));
_ruleEngine.Execute(objectToRunRulesOn);
}

public void Execute(T objectToRunRulesOn, bool trackData)
{
if (trackData)
{
// Create dummy ActivityExecutionContext and see that trackData is intercepted.
// Initialize with own activity and insert IWorkflowCoreRuntime

Type activityExecutionContext = typeof(Activity).Assembly.GetType("System.Workflow.ComponentModel.ActivityExecutionContext");
var ctor = activityExecutionContext.GetConstructor(BindingFlags.Instance BindingFlags.NonPublic, null,
new[] { typeof(Activity) }, null);
var activity = new InterceptingActivity();
var context = ctor.Invoke(new object[] { activity });

_ruleEngine = new RuleEngine(_ruleSet, typeof(T));
lock (SyncRoot)
{
InterceptingActivity.Track += InterceptingActivity_Track;
_ruleEngine.Execute(objectToRunRulesOn, (ActivityExecutionContext)context);
InterceptingActivity.Track -= InterceptingActivity_Track;
}
}
else
{
Execute(objectToRunRulesOn);
}
}

public List<TrackingEventArgs> GetRuleMessages()
{
return _ruleMessages;
}

private void InterceptingActivity_Track(object sender, TrackingEventArgs e)
{
#if DEBUG
Console.WriteLine("{0} Rule result of {1} = {2}", e.RulesetName, e.Args.RuleName, e.Args.ConditionResult);
#endif
_ruleMessages.Add(e);
}
}
}

To use the RuleHelper<>, do something like this, it's pretty self-explainatory:

// Call the rules helper
RuleHelper ruleHelper = new RuleHelper();
// Set the rules
ruleHelper.SetRules(ruleSet);
// Execute the rules.
ruleHelper.Execute(rr, true);
// Get the tracked rules
var ruleMessages = ruleHelper.GetRuleMessages();

Regards,

W.