Real-World WF

Best Practices For Windows Workflow Foundation Apps

Josh Lane

Code download available from the MSDN Code Gallery

This article discusses:

  • The workflow programming model
  • Workflows and services
  • Unit testing workflows
  • Plugging WF into your solution
This article uses the following technologies:
Microsoft .NET Framework 3.5, Windows Workflow Foundation, Visual Studio 2008

Contents

The Workflow Programming Model
Side Effect-Driven Programming
Services, Services, Services
Episodic Execution
Unit Testing Is Still Your Friend
A Runtime within a Runtime
It's Not All or Nothing
Plug and Play
Domain Modeling and Program Design
Additional Advice

Since its release as part of the Microsoft .NET Framework 3.0, I've spent a lot of time with Windows Workflow Foundation (WF), implementing systems with the technology and teaching others to do the same. These experiences have given me some insights into some best (and less-than-best) practices to consider when using WF to realize software solutions in the real world.

My experience has been that WF suffers from something of an identity crisis. Many developers I talk to understand, at some level, what's going on in WF: conditional logic, flow control, atomic actions, and so on.

"Yes, I think I understand. You drag shapes onto the design surface. These represent actions that execute in some sort of 'flowchart' sequence. Yes, that certainly is clever!" This can be followed by mild protests along the lines of "But what would I use this for?" or "But I can do this already in C# or Visual Basic!"

In the abstract, these are not unreasonable objections. But while it's true that the public face of WF is the drag-and-drop designer experience in Visual Studio, and that a superficial look at the features of WF will uncover capabilities seemingly found elsewhere, the real benefits of WF are found underneath the surface.

To appreciate the value of WF, it's useful to consider some historical context. Since its inception, a primary goal of the .NET Framework has been to raise the level of abstraction at which Windows software programs are constructed. COM, HRESULTs, smart pointers, MTAs, message pumps, thunking layers … the sheer number of non-domain-specific details a Windows programmer needed to grasp could be overwhelming.

Thankfully, the .NET Framework has succeeded in turning most of these details into historical trivia. Indeed, most of us developers take for granted that we can open up Visual Studio and immediately start writing code that maps directly to the problem at hand. We're happy to let the .NET runtime worry about issues such as memory management, enforcement of security policy, runtime resolution of code dependencies, and countless other details. As a result, many developers are far more productive today than they've ever been before.

WF represents a logical next step in this march toward higher levels of programming abstraction. WF programs execute within a special runtime environment that itself runs on the .NET CLR. The WF runtime environment imposes some restrictions on the developer authoring programs that target it. But in return WF offers a powerful, flexible, and extensible set of runtime services such as support for long-running code, where execution might span days or even months; support for pausing/resuming/canceling running programs; support for auditing and tracing program execution; and even modification of running program logic!

The heart of WF is its declarative programming model. That is, in WF you describe what you want your program to do, typically via the drag-and-drop WF designer in Visual Studio. However, you don't specify exactly how this work should be accomplished. Instead, the WF runtime uses your program description (ultimately, a hierarchical graph of control flow and activities) as a blueprint for execution of your desired logic.

The Workflow Programming Model

Spend five minutes with the workflow designer in Visual Studio and it becomes obvious that WF is a component technology. A component in WF is an activity. More concretely, it is any subclass of the Activity base class.

Activities are the unit of composition and execution in WF. Some activities—those that subclass CompositeActivity, which itself subclasses Activity—have child activities that execute as part of the parent activity's execution sequence. Indeed, a workflow in WF is nothing more than a graph of such activities, starting with a single root activity and expanding to arbitrary depth and complexity. The innate logic of each activity in the graph, combined with data manipulated by this logic, ultimately determines execution order and overall workflow behavior.

I like to think of WF activities as Lego blocks. With Legos, you combine individual blocks of varying color, size, and function (minimally useful on their own) to build a larger whole, greater than the sum of its parts. Similarly, in WF, you combine atomic activities that perform discrete functions to create higher-order sets of logic, which are then combined to realize still larger business processes, and so on.

One pattern common to component programming in general (and WF in particular) is to prefer composition over inheritance to achieve reuse. As you work with WF over time and build increasingly complex workflows, you'll more often find yourself composing programs from smaller, autonomous activities rather than creating deep class hierarchies along the lines of complex systems written in C# or Visual Basic.

You're still able to employ class inheritance as a reuse technique in WF. There's nothing wrong with deriving from SendEmailActivity to create, say, SendEncryptedEmailActivity. In this case, you're altering the fundamental nature of the action being performed, and inheritance is a natural technique for this. But if you need to conditionally send the e-mail only under certain circumstances or do other work before and after sending the e-mail, a better choice is to make SendEmailActivity a child of another CompositeActivity subclass (like SequenceActivity or IfElseActivity) and define the additional logic outside SendEmailActivity.

Figure 1 shows a composite activity that encapsulates a conditional e-mail action. This allows Send­EmailActivity to maintain a single responsibility (sending e-mails), thus increasing its chance for reuse in other workflows. It also allows you to reuse existing constructs in WF (support for conditional branching and sequential activity execution) instead of writing new code from scratch.

fig01.gif

Figure 1 A Custom Composite Activity with Conditional Logic

Side Effect-Driven Programming

In programming, a side effect is defined as an observable change to shared state that results from execution of some part of the program. Examples of this include a change in value to an object property, or a change to one or more values in a database table. Side effects are used all the time to accomplish real work in software.

In contrast to pure functional programming where shared state is avoided and method return values are the primary means of data flow and manipulation, everything interesting in a workflow involves a side effect of some kind. Within a workflow program there is no such thing as a return value; instead, you compare pre- and post-execution state for changes. You dictate control flow not by calling methods, but rather by composing activities alongside and within one another. Activity inputs and outputs are implicit in the design of the activity class (as properties), rather than explicitly defined via method parameters or return values. Data flows from one activity to another using built-in support for activity data binding. You also have the option of manipulating shared state by interacting with configured services in the logic of your custom WF activities.

The basic execution sequence for WF activities is to initialize activity state, execute activity, examine resultant activity state for changes, then repeat for all other activities. So it is precisely the side effects of activity execution ("What is the new value of the PurchaseOrderTotal property?" or "What database value just changed?") that tell you what just happened and what will happen next. You do have the potential for activities to manipulate state in ways undesirable for other downstream activities (however, note that the risk here is no worse than that inherent in any imperative language like C#), but you don't pay the performance costs associated with data copying.

Services, Services, Services

As a general purpose programming environment, WF is designed to integrate with external subsystems and code. The most direct way to accomplish this is to author custom workflow activities that make database or Web service calls or call into third-party libraries. In WF terminology, such external systems are known as services.

The idea behind WF services is simple: any library or functional element that is required by an activity in your workflow, but is not contained within the activity definition, is registered as a service with the WF runtime:

// create an instance of your service type...
  LoggingService logSvc = new LoggingService();

  // register it with the WorkflowRuntime instance...
  workflowRuntime.AddService( logSvc );

  WorkflowInstance instance = 
    workflowRuntime.CreateWorkflow( typeof( Workflow1 ) );

  instance.Start();

During execution, an activity can request access to the services it needs through the ActivityExecutionContext (activities never interact directly with their host runtime) and use the configured services to perform work, while letting the WF runtime and host manage service lifetimes:

protected override ActivityExecutionStatus Execute( 
    ActivityExecutionContext aec ) {
  // ActivityExecutionContext provides access to services...
  LoggingService logSvc = aec.GetService<LoggingService>();

  Debug.Assert( logSvc != null );
  logSvc.LogEvent( EvtType, Message ); 
  return ActivityExecutionStatus.Closed;
}

Typical service examples include logging components, data access layers, and proxies to external subsystems, though the possibilities are limitless.

The power of the WF service model stems from its simplicity. Literally any .NET object can be configured as a service. Note that some extra behaviors, such as service lifetime management, are available to services explicitly designed to subclass WorkflowRuntimeService. Also, I usually find it useful to expose services via an interface instead of through a concrete type; this makes it easier to vary the service implementation independent of the activities that depend on it.

So the key best practice here is the decoupling of a custom WF activity's core logic from the constituent pieces needed to accomplish its work. Additionally, you should refactor into a service logic that is stateful and is shared among multiple activities in a workflow (logging, for example); it would be cumbersome to pass a reference to the logging component down the activity graph during execution. This is further complicated for workflows serialized at some point during their execution cycle.

I'll mention one other bit of advice regarding services. Windows WF ships with an infrastructure known as local services for calling out to .NET class instances from activities, as well as handling .NET events inside an activity. You interact with these local services via the CallExternalMethod and HandleExternalEvent activities. They are essentially an extra layer on top of the base WF service infrastructure and provide the same benefits as raw services in a declarative style friendlier to WF host environments such as SharePoint. However, for more general-purpose hosts such as Windows Presentation Foundation (WPF) or ASP.NET, this extra layer can be more cumbersome to use than the equivalent raw service configuration. Keep in mind your anticipated hosting requirements before building solutions with local services.

A key principle to employ here is to leverage the power and flexibility of the WF service model to design well-factored, reusable workflow activities.

Episodic Execution

One feature that makes WF particularly well suited for general-purpose programming tasks is its support for asynchronous, non-blocking work. The very nature of modern Web service- and database-centric applications lends itself to asynchronous invocation, where a request is made to the service and then local resources associated with the requesting call are freed to process other work. Later, when the dispatched work is completed, execution can pick back up where it left off. ASP.NET asynchronous pages are one example of this technique.

When first executed by the WF runtime, the activity's Execute method is invoked. Execute returns ActivityExecutionStatus.Completed if the activity has performed all of its work within Execute. It returns ActivityExecutionStatus.Executing if it reaches a point where it's done as much work as possible, and is now waiting on some external stimulus, such as a response from a Web service or data­base call or an e-mail sent to a specific address, to continue.

Figure 2 shows the Execute method for an activity that queries an external service for a credit score, given a customer ID. Since credit scoring might take minutes or even hours to complete, the workflow invokes the service asynchronously and then goes idle waiting on a result. Before going idle, the activity registers a delegate to be invoked by the runtime when the stimulus arrives. This delegate is known as a continuation (literally, a "pointer to the rest of the program") and allows the activity author to define any logic needed to continue activity processing. The lifetime of a single workflow may consist of several such idle-continue state transitions.

Figure 2 The Execute Method

protected override ActivityExecutionStatus Execute( 
  ActivityExecutionContext aec ) {

  if ( string.IsNullOrEmpty( SSN ) ) {
    throw new InvalidOperationException( "SSN property value is invalid." );
  }

  ICreditScoring creditSvc = aec.GetService<ICreditScoring>();
  Debug.Assert( creditSvc != null );

  WorkflowQueuingService queueSvc = aec.GetService<WorkflowQueuingService>();
  Debug.Assert( queueSvc != null );

  Guid queueName = Guid.NewGuid();
  WorkflowQueue queue = queueSvc.CreateWorkflowQueue( queueName, false );
  queue.QueueItemAvailable += CreditScoreComputed;
  creditSvc.ComputeScoreAsync( SSN, queueName );

  return ActivityExecutionStatus.Executing;
}

The runtime receives notice of the external stimulus needed to wake an idle workflow through arrival of an item (any .NET object) on a workflow queue associated with the workflow instance. In the case of an asynchronous Web service call, the code waiting on the response obtains a reference to the queue and pushes any relevant data (presumably whatever was finally received from the Web service) onto the queue. The runtime monitors the queue and invokes the registered delegate; the delegate logic can obtain the queued item and perform any necessary processing.

The power of this approach stems from what happens to the workflow during idle time. Activities that return ActivityExecutionStatus.Executing from their Execute method (and are thus considered idle) are obviously doing no work for some period of time. This might be seconds, hours, days or even months of waiting on some external stimulus. During this time, there's no need for the workflow to remain in memory consuming machine resources that could otherwise be put to better use, so the WF runtime supports the ability to automatically unload such workflows from memory and serialize them to persistent storage using a persistence service, such as SqlWorkflowPersistenceService. When the external stimulus arrives, the WF runtime reloads the workflow and continues execution as normal.

Unit Testing Is Still Your Friend

Unit testing remains relevant to your life as a programmer targeting WF. In fact, the explicit compositional nature of WF is a natural fit for authoring a battery of comprehensive tests that exercise each piece of your solution in isolation of other pieces. I'll give you some higher-level information here, but be sure to check out Matt Milner's Foundations column in the November 2008 issue of MSDN Magazine (" Unit Testing Workflows And Activities ") for a deeper look at unit testing workflows.

A common misconception when working with WF is that you can only execute entire workflows (meaning, subclassed instances of SequentialWorkflowActivity). In fact, you can execute any activity instance as a standalone workflow, which makes it easy to use a generic activity test harness to execute and validate activities in isolation or in small subsets of a larger whole. Figure 3 shows such a test harness.

Figure 3 A Generic Workflow Test Harness

protected Dictionary<string, object> ExecuteActivity( 
  Type activityType,  
  IEnumerable<object> 
  services, 
  params object[] inputs ) {

  Dictionary<string, object> outputs = null;
  Exception ex = null;

  using ( WorkflowRuntime workflowRuntime = new WorkflowRuntime() ) {
    AutoResetEvent waitHandle = new AutoResetEvent( false );

    workflowRuntime.WorkflowCompleted += 
      delegate( object sender, WorkflowCompletedEventArgs e ) {

      outputs = e.OutputParameters;
      waitHandle.Set();
    };

    workflowRuntime.WorkflowTerminated += 
      delegate( object sender, WorkflowTerminatedEventArgs e ) {

      ex = e.Exception;
      waitHandle.Set();
    };

    foreach ( object svc in services ) {
      workflowRuntime.AddService( svc );
    }

    Dictionary<string, object> parms = new Dictionary<string, object>();

    for ( int i = 0; i < inputs.Length; i += 2 ) {
      Debug.Assert( inputs[ i ] is string );

      parms.Add( (string) inputs[ i ], inputs[ i + 1 ] );
    }

    WorkflowInstance instance = 
      workflowRuntime.CreateWorkflow( activityType, parms );

    instance.Start();

    waitHandle.WaitOne();

    if ( ex != null ) {
      Assert.True( false, ex.Message );
      return null;
    }
    Else {
      return outputs;
    }
  }
}

The basic state-oriented test pattern of "data in/execute code/make assertions against data" works just fine for WF with a generic test harness. The WF runtime supports auto-population of root activity property values using a generic Dictionary keyed by property name. It also allows you to examine root activity property values after activity execution has completed. Even if your activity will ultimately be nested several levels inside a workflow hierarchy and relies on data binding to initialize its property values, you can test the activity in isolation of others and simulate the full range of expected and error conditions for that activity.

WF also plays nicely with more advanced unit testing techniques such as object mocking, where the emphasis is on isolating tested code from its dependencies, as well as validation of expectations on those dependencies (for example, "When executing the code for this test, I expect the Foo() method for this dependency to be called exactly 3 times."). Here's an example:

// mock the IBanking interface...
var bankingMock = new Mock<IBanking>();

// specify that you expect AddCustomer to 
// be called with the customer instance...
bankingMock.Expect( bank => bank.AddCustomer( customer ) );

// execute WF harness, configuring the mock as a service...
ExecuteActivity( typeof( AddCustomerActivity ), 
  new List<object>() { bankingMock.Object }, "Customer", customer );

If you're familiar with unit testing and object mocking, you might have already guessed that WF services are perfect candidates for mocking in tests that target WF activities. WF services are dependencies by their very nature and should be factored out of such unit tests.

A Runtime within a Runtime

WF is more than a component-based extensible programming environment. It also incorporates an execution engine built on top of the CLR. This engine provides many services useful to workflow programs, such as work scheduling, automatic serialization/deserialization during idle states, and propagation of data from parent to child activities via data binding, among other things. But you must take care to play by the WF rules; naïve workflow implementations can perform poorly in this environment, or not at all.

Perhaps the most important thing to understand about the WF runtime is that it imposes no specific threading requirements on its host code. Rather, it's designed to integrate with any existing threading model, be it single-threaded or multithreaded.

Scheduling workflow execution occurs via a service configured against the WF runtime. The DefaultWorkflowSchedulerService executes workflows on the .NET ThreadPool, while the ManualWorkflowSchedulerService allows you to specify exactly which thread you want to use (useful with WF hosts that themselves dispatch work to the ThreadPool, like ASP.NET). Though rarely needed in practice, you also have the option of providing your own custom scheduling implementation.

Note that, regardless of threading model, individual workflows never execute on more than one thread at any given moment. This is a conscious design choice in the WF runtime execution model. If you need to invoke multithreaded code within a workflow, your best bet is to do it in a custom WF service.

Unlike traditional stack-based programming environments such as C# or Visual Basic, workflows execute asynchronously with respect to their calling code. That is, when a workflow completes execution, control is not automatically returned to the point of invocation of the workflow. Rather, each workflow instance becomes a separate logical fork in your program.

Managing state for workflows running in WF introduces the potential for problems without an understanding of the WF runtime. Traditional POCO (Plain Old CLR Objects) programming uses class member fields and properties for data storage. You build WF activities in the same manner, but in the case of episodic workflows configured to persist during idle states, a single logical workflow lifetime might be realized via multiple physical activity graph instantiations (as depicted in Figure 4 ) and across multiple applications or even multiple machines.

Figure 4 Logical vs. Physical Workflow Lifetime, for Episodic Workflows

While WF supports automatic serialization of activity graphs and state during persistence, it's important to note that it only works for a subset of data types (no arrays, and limited support for generics). Hand-authored custom serialization is possible via serialization surrogates. You can also override Activity.OnActivityExecutionContextLoad and Activity.OnActivityExecutionContextUnload to do custom processing per activity for each instantiation and teardown of a workflow object graph.

Remember that WF is a runtime unto itself. Avoid common pitfalls by developing a basic understanding of the WF threading, scheduling, and serialization concepts.

It's Not All or Nothing

It's a common anti-pattern when first adopting a new technology to apply it everywhere, sometimes to the exclusion of other options, the proverbial "when wielding a hammer, everything looks like a nail" syndrome. We've all been guilty of this at some point. And while this is certainly possible with WF, its nature as a low-level plumbing technology with many integration points and extensibility hooks allows you to apply it in many distinct problem domains.

WF works equally well in highly multithreaded environments, such as ASP.NET, or in desktop client applications built with Windows Forms or WPF. Common uses for WF in these scenarios include implementation of UI-driven logic, where you implement user interface event-handling logic in a workflow instead of imperative code; task-oriented asynchronous background work, such as fetching and processing Web search results or manipulating large files on disk, typically invoked on a separate thread; and page flow, in which workflow logic dictates UI navigation for ASP.NET or smart clients.

Figure 5 shows the design surface for a Microsoft sample PageFlow application . Each top-level activity in the workflow defines the view (the Web page or application form) to display, as well as any custom processing for each view. The allowed transitions between views are also depicted here. Contrast a clean graphical representation like this with dozens of lines of C# or Visual Basic to codify the same information and you can see where WF has obvious benefits.

fig05.gif

Figure 5 A Workflow for Governing User Interface Navigation Steps
(Click the image for a larger view)

WF and Windows Communication Foundation (WCF) are natural technology mates in service-oriented applications. A workflow is, in essence, a service that you invoke to perform a specific function, and WCF is designed for exposing services for consumption. To make it easy on you, the .NET Framework 3.5 ships with the SendActivity and ReceiveActivity activities that make WF-to-WCF integration possible right out of the box. You use ReceiveActivity to declaratively expose WF workflows behind a WCF address/binding/contract facade. The workflow is the service implementation, and it is invoked by the WCF integration plumbing as requests are received.

You can also expose long-running workflows this way, where a single workflow instance might expose multiple service operations and go through several execute/idle sequences. WCF binding types (BasicHttpContextBinding, WsHttpContextBinding, or NetTcpContextBinding) are used to correlate messages sent from clients to specific in-flight workflow instances.

Finally, as you might guess, SendActivity allows you to configure outbound WCF client connections from your workflow to external services with relative ease.

As a .NET integration technology, WF programs can interact with any library or technology consumable by .NET code. Your existing technology investments needn't be discarded when implementing application logic in WF; to the contrary, the real value of the technology lies in enabling greater and more productive use of existing technology assets.

WF is as much an integration technology as a means unto itself. WF is being used as a foundation technology within the Microsoft products that employ a workflow product. These include SharePoint, the Dynamics products, and the upcoming release of Office Communications Server and Office Project. This common usage allows for much easier integration between the technologies and your WF application. Expect to use it as part of a greater whole.

Plug and Play

In order to maximize integration opportunities, extensibility is a fundamental tenet of WF. Key pieces of the core infrastructure are configurable—or even replaceable with custom implementations. And while WF ships with numerous built-in activities for control flow, transactional semantics, and services communication (among others), none of these activities are required for use in your WF programs, and none of them uses special features unavailable to the WF programming community at large. Your own custom activities (or those of third-party vendors) are inherently no more or less capable than anything that ships out of the box.

Action-oriented activities that map to domain-specific behaviors are obvious choices for custom implementation. A workflow used by a mortgage company to originate loans might contain custom activities for requesting flood insurance quotes, computing property taxes, and obtaining applicant credit scores, among others. These are useful and necessary, but the more interesting cases are custom composite activities where the emphasis is on customized control flow instead of realizing a specific domain function. WF ships with several standard control flow composites such as IfElse­Activity and WhileActivity. But with WF, you can realize more exotic cases, such as execution of composite child activities based on some configurable priority level, or perhaps use of a dependency network to determine execution order (see Figure 6 ).

Figure 6 Using a Dependency Network

[Designer( typeof( DependencyNetworkActivityDesigner ) )]
[ActivityValidator( typeof( DependencyNetworkActivityValidator ) )]
public partial class DependencyNetworkActivity : SequenceActivity {

  DependencyProperty.RegisterAttached( "RunAfter",
    typeof( string ), typeof( DependencyNetworkActivity ),
    new PropertyMetadata( DependencyPropertyOptions.Metadata ) );

  public static object GetRunAfter( object depObj ) {
    return ( (DependencyObject) depObj ).GetValue( RunAfterProperty );
  }

  public static void SetRunAfter( object depObj, object value ) {
    ( (DependencyObject) depObj ).SetValue( RunAfterProperty, value );
  }

  public DependencyNetworkActivity() {
    InitializeComponent();
  }

  private IList<Activity> _orderedList = null;
  private int _currentIndex = 0;

  protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec ) {
    if ( this.EnabledActivities.Count == 0 ) {
      return ActivityExecutionStatus.Closed;
    }
    else {
      _orderedList = DependencyNetworkActivityValidator.ComputeOrderedList( this );

      Debug.Assert( _orderedList != null );
      Debug.Assert( _orderedList.Count == this.EnabledActivities.Count );

      ScheduleNextChild( aec );

      return ActivityExecutionStatus.Executing;
    }
  }

  private void ScheduleNextChild( ActivityExecutionContext aec ) {
    Debug.Assert( aec != null );

    Debug.Assert( _orderedList != null );
    Debug.Assert( _currentIndex < _orderedList.Count );

    Activity child = _orderedList[ _currentIndex++ ];

    Debug.Assert( child != null );

    child.Closed += ChildDone;

    aec.ExecuteActivity( child );
  }

  private void ChildDone( object sender, ActivityExecutionStatusChangedEventArgs e ) {
    ActivityExecutionContext aec = ( sender as ActivityExecutionContext ); 
    e.Activity.Closed -= ChildDone;

    if ( _currentIndex < _orderedList.Count ) {
      ScheduleNextChild( aec );
    }
    else {
      aec.CloseActivity();
    }
  }
}

In addition, almost all of the various runtime services that ship with WF are extendible or replaceable. You're free to (for example) implement your own workflow persistence layer on top of another storage medium, or perhaps implement a workflow scheduler that executes against a custom thread pool implementation instead of the built-in .NET ThreadPool. Any such custom service is configured in exactly the same way as all other WF services, so the model is consistent and straightforward.

Ultimately, the goal for WF is to remain flexible in the face of evolving technologies and application architectures, instead of mandating a rigid set of requirements that might otherwise render the technology less broadly applicable.

Additional WF Resources

Workflow Tips and Tricks

Workflow Services (Windows Workflow Foundation and Windows Communication Foundation)

Build Workflows To Capture Data And Create Documents

Loading Workflow Models in WF

Domain Modeling and Program Design

One of the more difficult aspects of building software with WF is mapping elements of your problem domain to elements of a workflow. The primary issue is the control flow-centric programming model of WF in which—unlike traditional imperative, or object-oriented programming—the atomic elements of your workflow (activities) don't represent entities in your problem domain, but rather actions or processing steps. It's a subtle but important distinction, and it makes what are already non-trivial tasks (domain modeling and program design) even more challenging, especially for WF newcomers.

It's one thing to understand the mechanics of the WF programming model—activities, services, episodic execution, and the rest—but it's another thing entirely to take a functional specification or a bunch of use cases and turn that into one or more efficient, well-factored, robust workflows. What domain concepts map to an activity versus service versus an entire workflow? What data should flow through the workflow, and how? Where can work be done in parallel, and where must the workflow be designed to go idle, waiting on user input?

Truth be told, it's impossible to sufficiently answer such questions in the space of a few paragraphs. But I offer you here a few general guidelines that have served me well. As with any discussion of modeling and design, take the advice that follows as a starting point and not a complete treatment of the topic, and don't assume this is the only way to achieve WF design nirvana. It isn't.

First, start your modeling phase as you normally would with use cases, actors, and domain elements given up-front design consideration suitable for your project. Use any approach that works for you; there's no need for WF-specific accommodations here.

Next, model your actors (Customer, BankManager, and so on) and your domain elements (PurchaseOrders, InsurancePolicies, AcmeWidgets) as objects. As always, give careful consideration to the data encapsulated by each item. Expose data with properties, as necessary. Stub out actor methods, but don't implement them just yet.

At this point, you have a decision to make. Is your intended design WF-centric, such that all significant behavior will be represented as distinct activities in one or more workflows? Or do you want to use WF in a more targeted fashion, implementing key aspects in WF while keeping the rest as more traditional .NET code? Don't consider it a binary yes-or-no question; instead, give deliberate consideration to where on the spectrum between all WF and no WF you want to be. The strengths of WF—modularity, visibility, and feature set—will be more beneficial to you in some areas (complex and continuously changing business logic) than in others (user interface layout or data access layers).

Now consider the operations defined by each domain element. In traditional object-oriented programming, these will map to public methods of concrete types. In WF, such actions (say, "withdraw money from bank account" or "add item to shopping cart") usually map to individual custom activities that operate on one or more defined objects. The more you move toward WF on the continuum, the more actions you'll implement as WF activities, and the fewer you'll implement as traditional class methods.

Use cases will define an ordered set of actions performed by an actor to achieve a specific goal, as well as contingencies for error conditions. In C# or Visual Basic, this might manifest itself through use of the Strategy or Template Method patterns, resulting in methods hanging off of actor type definitions. With WF, I like to map use cases to custom composite activities consisting of domain element action activities and other activities that define conditional logic in the use case. Figure 7 shows a custom composite activity (AddCustomerWithAuditActivity) that contains no code-based logic; all steps are implemented as a series of custom activity invocations.

fig07.gif

Figure 7 A Custom Composite Activity with No Code-Based Logic

Since your use cases are now defined as custom composite activities, you can then define larger business processes ("do month-end reconciliation" or "project next fiscal-year budget") as workflows that aggregate these custom composites. Actor behaviors can be defined as class methods that accept all necessary input and launch the appropriate workflow, passing data into the workflow as needed and harvesting results. Data can be passed via data binding or service invocation, as appropriate.

Domain modeling and code design in WF have some similarities, but also several key differences to the equivalent tasks in imperative .NET programming. Anticipate a learning curve and some refactoring opportunities as inevitable by-products of your first WF projects.

Additional Advice

I'll round out my advice with some important points that didn't otherwise merit a full section of their own:

First, minimize use of CodeActivity. It can be useful for one-off "glue" code to ensure proper data flow between activities, but otherwise should be replaced by a custom activity specific to the task at hand.

When integrating workflows with other subsystems, it is better to expose WF functionality indirectly through an interface. This promotes loose coupling and makes your workflow code easier to consume and easier to test. It also then becomes easier to vary the implementation of WF logic over time.

Don't neglect to implement validators for your custom activities. The WF activity composition model supports optional validation logic for allowed versus disallowed configuration of activities. You implement this by subclassing ActivityValidator and attributing the target activity type. See the accompanying code download for an example validator implementation.

Know that the boilerplate Visual Studio wizard-generated workflow hosting code is useful for ad hoc invocation and testing of workflows, but little else. The most interesting and useful workflows might never execute from a command line!

For maximum runtime agility, you should consider use of a dependency injection container. Check out StructureMap or Spring.NET for WF custom service configuration.

If your workflow executes business rules using PolicyActivity, consider extending the available grammar with domain-specific keywords by subclassing RuleAction. This increases expressiveness and comprehension of authored rules.

And last, but not least, remember to design your workflow hosting environment so that it is continuously available to service responses from external systems, bound for idle workflows. Your workflow may be idle, but if your workflow host is not running, the external system response may never be handled, and your idle workflow never awoken!

For a broad technology like WF, any list of best practices or key principles is liable to be incomplete, or applicable to varying degrees depending on circumstance. The guidance found here comes from my own experiences, as well as those of my colleagues and students; it is broadly, but not universally, applicable. Consider your specific problem domain and constraints when deciding how my advice might best fit (or not fit) your needs.

Josh Lane is Principal Architect for business rule engine vendor InRule Technology Inc. and a .NET instructor for DevelopMentor. He lives in Atlanta, Ga.