Ideas, designs and solutions with explanations and examples.

Monday, June 16, 2008

Domain Specific Language (DSL) with Workflow, Part 2

In the previous article I presented some basic ideas for creating DSL activities in Workflow. In this part I want to deep into the challenges I faced during the implementation and share the solutions. As usual, let's start from the requirements:

  1. DSL is a set of services, each one implemented by a separate function.
  2. For each such function provide a specialized Activity.
  3. All the return/out/ref parameters should work as expected.
  4. Developing of those Activities should be simple and provide Activity validation.

The first requirement is easy - I can have a separate method for each DSL service in my ExternalDataExchange interface:

// The ExternalDataExchange attribute is a required attribute 
// indicating that the local service participates in data exchange with a workflow
[ExternalDataExchange]
public interface ITaskService
{
int CreateTask(string taskId, string assignee, ref string text);

//...
}


To meet the next requirements I wanted to provide a base class, that by simple derivation from it and decorating my properties with some attribute, the desired DSL activity will be created:



[ToolboxItemAttribute(typeof(ActivityToolboxItem))]
public class CreateTask : DslActivity<CreateTask>
{
// Properties on the task
public static readonly DependencyProperty ReturnValueProperty = DependencyProperty.Register("ReturnValue", typeof(System.Int32), typeof(CreateTask));
public static readonly DependencyProperty AssigneeProperty = DependencyProperty.Register("Assignee", typeof(System.String), typeof(CreateTask));
public static readonly DependencyProperty TaskIdProperty = DependencyProperty.Register("TaskId", typeof(System.String), typeof(CreateTask));
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(System.String), typeof(CreateTask));

public CreateTask()
// Map the activity to the service method
: base(typeof(ITaskService), "CreateTask")
{
}

// Property, mapped to the return value of the method
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(true)]
// "Well Known" name for the return value
[DslParameter(Name = "(ReturnValue)")]
[Category("ReturnValue")]
public int ReturnValue
{
get
{
return ((int)(base.GetValue(ReturnValueProperty)));
}
set
{
base.SetValue(ReturnValueProperty, value);
}
}

// Property, mapped to the 'assignee' parameter of the method
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(true)]
[DslParameter(Name = "assignee")]
[Category("Parameters")]
public string Assignee
{
get
{
return ((string)(base.GetValue(AssigneeProperty)));
}
set
{
base.SetValue(AssigneeProperty, value);
}
}

// Other properties - mappings to the rest method parameters


In this way development of DSL activities is simple and straightforward. Now let's look into the base class to see how it's achieved. All the magic is done inside the constructor:



// Initializes parameters by reflecting the typeof(T) members
// and looking for DslParameterAttribute attribute.
private static readonly IList<ParameterInfo> _parameters = GetParameters(typeof(T));

public DslActivity(Type interfaceType, string methodName)
{
// Set up the interface and method
InterfaceType = interfaceType;
MethodName = methodName;

for (int i = 0; i < _parameters.Count; i++)
{
ParameterInfo pi = _parameters[i];
// This is the main 'trick' - create a binding to 'this' Activity
// with path set to the relevant property name
ActivityBind bind = new ActivityBind("/Self", pi.Path);
WorkflowParameterBinding binding = new WorkflowParameterBinding(pi.ParameterName);
binding.SetBinding(WorkflowParameterBinding.ValueProperty, bind);
ParameterBindings.Add(binding);
}
}


That's all, hope you enjoyed! Download here the sample code for this article.

Saturday, June 14, 2008

Domain Specific Language (DSL) with Workflow

Workflow provides two basic activities for interaction with outer world: CallExternalMethodActivity and HandleExternalEventActivity. But sometimes we want to limit our workflow to not use those, since they are too "generic". Instead, we want to provide our set of Domain Specific activities.

The simplest way to achieve this goal is by derivation, providing the required specialization.

Here is a link to a simple solution demonstrating the technique. In this example I create 2 application tasks and wait for completion of any of them. All the work is done with custom activities, defining my DSL.

Tuesday, June 3, 2008

Workflow (WF) RuleEngine without Workflow

Rule engine is a very useful feature and not always we want the entire Workflow to get into play.

Here is a simple code snippet showing how one can use RuleCondition/RuleSet outside the Workflow scope:

WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();
using (Stream s = typeof(Program).Assembly.GetManifestResourceStream("SimplePolicyWorkflow.rules"))
{
RuleDefinitions rd = (RuleDefinitions)serializer.Deserialize(XmlReader.Create(s));
RuleCondition c = rd.Conditions[0]; // or RuleSet

//typeof(rule context) should be passed
RuleValidation v = new RuleValidation(typeof(Program), null);

if (c.Validate(v))
{
//rule context instance should be passed
RuleExecution exe = new RuleExecution(v, new Program());
c.Evaluate(exe);
}
}

Sunday, June 1, 2008

Self Code Review Methodology

When I worked as a team leader I didn't want any code to get into the product without the code review in addition to the unit tests added. But good code review takes time, so I started thinking how I can optimize the process...

First, I came to a conclusion that a review actually consists of two parts:

  • Code logic. This is best done by challenging. If the developer quickly and correctly answers to questions, he probably thought about that scenario and covered it. If not, we open the relevant code and check its logic in depth. This gives the opportunity to concentrate on less 'polished' code, analyze it deeper and correct more issues.
  • Code sanity, which includes:
    • Code format - ensured by IDE.
    • Code style - which actually is a routine check list. Bingo! Why not have this check list written and given to the developers to do themselves to save the reviewer's time?

Below is the check list I created, please leave your comments...

For each added field
  1. Consider type safety. I.e. when the field is used, no cast is required.
  2. Ensure its access modifier is private.
  3. Ensure its value cannot be computed by other means (using other fields/methods). If yes, it means this field is for caching purposes or used by some setter. If it's for caching:
    • Ensure this caching is required and comment this in code.
    • Ensure it is always in sync with computed value and comment how this is achieved in code.
  4. Consider marking this field 'readonly'. If not possible, consider refactoring resulting this field becomes 'readonly'.
For each added property
  1. Ensure it has a getter.
  2. Consider removing a setter.
  3. Ensure set/get parity, i.e. if some value is set, the same one is get.
  4. Ensure sequential gets return a same logical value.
For each added property/method
  1. Consider minimizing its access scope (private static <--> public virtual)
  2. Ensure all the arguments are checked and relevant exception are thrown.
For each added class
  1. Consider minimizing its access scope (private sealed inner class <--> public not sealed).
  2. Ensure it has a minimal set of constructors.
  3. Consider refactoring to have one constructor with initialization logic and other forwarding to it. If needed, add a private constructor.
  4. If there is an override for Equals/Hashcode methods, ensure they come in pair and both are computed from the same data.
For each local variable
  1. Consider type safety. I.e. when the variable is used, no cast is required.
  2. Minimize variable scope.
  3. Ensure proper clean up in finally blocks; usually initailization should be right before the try block and not inside it.
For each added line of code
  1. Consider implicit impacts (boxing, objects creation, computation).
  2. Ensure that every System.SystemException derived exception can be possibly thrown is by intention. (NullReference, Cast etc).
For each 'lock' statement
  1. Consider using a framework class, which has the required synchronization built-in. For example, if you need a synchronized Hashtable, don't do locking yourself, but create it with Hashtable.Synchronized() / SynchronizedKeyedCollection<K, T>.
  2. Otherwise ensure the design is discussed with your manager.