Developing Runtime Services


By now, the fact that Windows Workflow Foundation is extremely extensible should be prevalent in your mind. The runtime services architecture is no exception to this rule. Of course, there may be times when the functionality provided out of the box does not meet the needs of a given project or organization. In such situations, you are encouraged to extend the base infrastructure.

Developing Scheduling Services

You can develop custom scheduling services to allow workflows to start based on logic that is not provided in DefaultWorkflowSchedulerService or ManualWorkflowSchedulerService. The following is a skeleton class inheriting from WorkflowSchedulerService:

  public class CustomWorkflowSchedulerService : WorkflowSchedulerService {     protected override void Cancel(Guid timerId)     {     }     protected override void Schedule(WaitCallback callback,         Guid workflowInstanceId, DateTime whenUtc, Guid timerId)     {     }     protected override void Schedule(WaitCallback callback,         Guid workflowInstanceId)     {     } } 

In this example, the Cancel method provides the logic to stop a workflow instance specified by a Guid value from running. The two Schedule overloads implement logic to queue the workflow instances for execution.

Developing Work Batch Services

Although the two work batch services included out of the box contain the functionality needed for most situations, the API allows you to develop your own custom batch service by inheriting from the WorkflowCommitBatchService class. The following code shows the skeleton of a custom batch service class. Basically, you just need to override the CommitWorkBatch service and implement your custom logic. You may want to do this if the transactional behaviors of the two other services do not meet your needs.

  public class CustomWorkflowCommitWorkBatchService : WorkflowCommitWorkBatchService {     protected override void CommitWorkBatch(         CommitWorkBatchCallback commitWorkBatchCallback)     {         // put your custom logic here    } } 

Developing Persistence Services

You can develop custom persistence services when the SQL Server persistence behavior is not adequate or appropriate for a particular project. For example, a disconnected application that is running on a user’s laptop in the field may not have the option of running SQL Server. In such a scenario, persisting workflow state to the filesystem may be the next best thing. This section covers the necessary steps for building a custom persistence service. In addition, an example of what a file persistence service might look like is showcased.

The WorkflowPersistenceService abstract class defines several methods that must be implemented in a custom persistence service. The SaveWorkflowInstanceState and LoadWorkflowInstance State methods are responsible for keeping track of a workflow instance’s state when it goes idle or is awakened from its sleep.

In addition, these two methods are responsible for implementing locking functionality. LoadWorkflow InstanceState needs to mark the persisted state as locked so that other workflow hosts are aware that the instance is spoken for at the moment. Furthermore, when this method attempts to load a workflow instance, it needs to check that same flag so that it doesn’t step on anyone’s toes.

SaveWorkflowInstanceState should respect the unlock parameter that is passed by the runtime. This variable is set to true after a workflow instance has already been persisted once before and subsequently loaded, and therefore locked.

LoadCompletedContextActivity and SaveCompletedContextActivity are responsible for managing the state of activity scopes related to compensation in transactions. The behavior implemented in these two methods is similar to the workflow instance methods that were just covered. The difference is the state that should be saved and subsequently restored represents a completed activity scope that needs to be restored if a transaction is rolled back.

The UnloadOnIdle method takes an Activity instance as its sole parameter and returns a Boolean value that indicates whether or not workflow instances should be unloaded and persisted when they run out of work and become idle. Depending on how you want your custom service to behave, you could either return a hard-coded true or false, or allow the host to set a variable indicating the unload functionality.

Finally, the UnlockWorkflowInstanceState method should implement behavior to unlock a currently locked workflow instance. To implement locking in a persistence service, you apply a flag or marker to a workflow instance’s metadata. The unlock method simply removes or unsets this marker.

The following code is a sample implementation of what a file persistence service might look like. FileWorkflowPersistenceService writes workflow state data to a directory specified during the object’s construction.

  public class FileWorkflowPersistenceService     : WorkflowPersistenceService, IPendingWork {     private bool unloadOnIdle;     private string persistenceDirectory;     public string PersistenceDirectory     {         get { return persistenceDirectory; }     }     public FileWorkflowPersistenceService(string persistenceDirectory,         bool unloadOnIdle)     {         this.unloadOnIdle = unloadOnIdle;         this.persistenceDirectory = persistenceDirectory;         if (this.persistenceDirectory.EndsWith("\\"))         {             this.persistenceDirectory = this.persistenceDirectory.Substring(0,                 this.persistenceDirectory.Length - 1);         }     }     protected override Activity LoadCompletedContextActivity(Guid scopeId,         Activity outerActivity)     {         string fileName = this.GetScopeFilePath(scopeId);         byte[] data = this.ReadData(fileName);         this.EnsureDeleteInstanceFile(fileName);         return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(             data, null);     }     protected override Activity LoadWorkflowInstanceState(Guid instanceId)     {         string fileName = GetInstanceFilePath(instanceId);         if (Path.GetFileNameWithoutExtension(fileName).EndsWith("_lock"))             throw new WorkflowOwnershipException(instanceId);         byte[] data = this.ReadData(fileName);         this.LockInstanceFile(fileName);         return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(             data, null);     }     protected override void SaveCompletedContextActivity(Activity activity)     {         byte[] data =             WorkflowPersistenceService.GetDefaultSerializedForm(activity);         WorkflowStatus status =             WorkflowPersistenceService.GetWorkflowStatus(activity);         PendingWorkItem workItem = new PendingWorkItem(             PendingWorkItem.WorkType.SaveCompletedContext,             data,             WorkflowEnvironment.WorkflowInstanceId,             status);         WorkflowEnvironment.WorkBatch.Add(this, workItem);     }     protected override void SaveWorkflowInstanceState(         Activity rootActivity, bool unlock)     {         byte[] data =             WorkflowPersistenceService.GetDefaultSerializedForm(rootActivity);         WorkflowStatus status =             WorkflowPersistenceService.GetWorkflowStatus(rootActivity);         PendingWorkItem workItem = new PendingWorkItem(             PendingWorkItem.WorkType.SaveInstanceState,             data,             WorkflowEnvironment.WorkflowInstanceId,             status);         WorkflowEnvironment.WorkBatch.Add(this, workItem);     }     protected override bool UnloadOnIdle(Activity activity)     {         return this.unloadOnIdle;     }     protected override void UnlockWorkflowInstanceState(Activity rootActivity)     {         Guid id = WorkflowEnvironment.WorkflowInstanceId;         this.EnsureUnlockInstanceFile(id);     }     // helper methods     private void WriteData(byte[] data, string filePath)     {         try         {             FileStream stream = new FileStream(filePath, FileMode.OpenOrCreate);             stream.Write(data, 0, data.Length);             stream.Close();         }         catch(Exception ex)         {             throw new PersistenceException("Could not write the file.", ex);         }     }     private byte[] ReadData(string filePath)     {         try         {             FileInfo info = new FileInfo(filePath);             byte[] data = new byte[info.Length];             FileStream stream = new FileStream(filePath, FileMode.Open);             stream.Read(data, 0, data.Length);             stream.Close();             return data;         }         catch(Exception ex)         {             throw new PersistenceException("Could not read the file.", ex);         }     }     private string GetInstanceFilePath(Guid id)     {         string[] files = Directory.GetFiles(this.persistenceDirectory,             id.ToString() + "*.wf");         if (files.Length > 1)             throw new PersistenceException("File confusion!");         if (files.Length == 0)             return null;         return files[0];     }     private string GetScopeFilePath(Guid scopeId)     {         string fileName = this.persistenceDirectory +             "\\scopes\\" + scopeId.ToString() + ".wf";         if (!File.Exists(fileName))             throw new PersistenceException("Could not file the scope file.");         return fileName;     }     private void LockInstanceFile(string currentFileName)     {         try         {             string newFileName = Path.GetDirectoryName(currentFileName) +                 "\\" + Path.GetFileNameWithoutExtension(currentFileName) +                 "_lock.wf";             File.Move(currentFileName, newFileName);         }         catch (Exception ex)         {             throw new PersistenceException("Could not rename file.", ex);         }     }     private void EnsureUnlockInstanceFile(Guid id)     {         try         {             string oldFileName = this.persistenceDirectory +                 "\\" + id.ToString() + "_lock.wf";             if(File.Exists(oldFileName))             {                 string newFileName = id.ToString() + ".wf";                 File.Move(oldFileName, newFileName);             }         }         catch (Exception ex)         {             throw new PersistenceException("Could not rename file.", ex);         }     }     private void EnsureDeleteInstanceFile(string fileName)     {         try         {             if (File.Exists(fileName))             {                 File.Delete(fileName);             }         }         catch(Exception ex)         {             throw new PersistenceException("Could not delete the file.", ex);         }     }     // IPendingWork Members     public void Commit(Transaction transaction, ICollection items)     {         foreach (PendingWorkItem item in items)         {             switch (item.WorkItemType)             {                 case PendingWorkItem.WorkType.SaveInstanceState:                     string filePath = this.GetInstanceFilePath(item.Guid);                     if (item.InstanceStatus != WorkflowStatus.Completed &&                         item.InstanceStatus != WorkflowStatus.Terminated)                     {                         if (filePath == null)                             filePath = this.persistenceDirectory + "\\" +                                 item.Guid.ToString() + ".wf";                         this.WriteData(item.Data, filePath);                     }                     else                     {                         this.EnsureDeleteInstanceFile(filePath);                     }                     break;                 case PendingWorkItem.WorkType.SaveCompletedContext:                     this.WriteData(item.Data, this.persistenceDirectory +                         "\\scopes\\" + item.Guid.ToString() + ".wf");                     break;             }             this.EnsureUnlockInstanceFile(item.Guid);         }     }     public void Complete(bool succeeded, System.Collections.ICollection items)     {         // do nothing...     }     public bool MustCommit(System.Collections.ICollection items)     {         return true;     } } 

To ensure consistency in workflow instances, the persistence service implements the IPendingWork interface. This interface enables the code to queue chunks of work that will be performed at a time appropriate for maintaining a stable environment. The IPendingWork.Commit method is called when the queued work items are ready to be performed.

The service also supports instance locking. In this implementation, locking is indicated by renaming a workflow instance’s file to include a lock suffix. This marker is checked whenever the service attempts to load an instance from the filesystem. If the service finds the lock flag, a WorkflowOwnership Exception is thrown. If the lock flag is not present and the workflow instance is successfully loaded, the service is responsible for adding the suffix so that other workflow hosts are not able to load the same instance. Finally, when the workflow instance is saved again and persisted, the lock flag should be removed.

The Commit method of the IPendingWork interface is passed a collection of objects that generally represent context related to the work to be done. For this example, a class was developed to hold the necessary information to perform the required persistence service tasks. The PendingWorkItem class is shown in the following code listing. The nested WorkType enumeration holds values that indicate what type of work should be performed in the IPendingWork.Commit method.

  internal class PendingWorkItem {     private WorkType workItemType;     private byte[] data;     private Guid guid;     private WorkflowStatus status;     public WorkType WorkItemType     {         get { return this.workItemType; }     }     public byte[] Data     {         get { return this.data; }     }     public Guid Guid     {         get { return this.guid; }     }     public WorkflowStatus InstanceStatus     {         get { return status; }         set { status = value; }     }     public PendingWorkItem(WorkType workItemType, byte[] data,         Guid guid, WorkflowStatus status)     {         this.workItemType = workItemType;         this.data = data;         this.guid = guid;         this.status = status;     }     public enum WorkType     {         SaveInstanceState,         SaveCompletedContext     } } 

Although this example shows a fairly interesting application of the persistence framework, several things could have been done to make it better. First, the current implementation does not clean up unused completed activity scopes after a workflow instance is completed or terminated. To add this functionality, a workflow instance ID needs to be tied to each activity scope so that these files can be deleted after they are no longer needed. You can accomplish this by prefixing each scope file name with the workflow instance ID to which it is associated.

Another potential enhancement for FileWorkflowPersistenceService would be to add code to load unloaded instances when child timers expire, just as the SqlWorkflowPersistenceService does. This code needs to check each workflow that is saved, get its next timer expiration, and then add the expiration to a list of other workflow instances’ expirations. This list should be sorted so that the service fires an event to load the workflow instance with the next expiration.

There are probably other ways to make the FileWorkflowPersistenceService more functional and solid. However, this should be a good start to inspire a better rendition.

Developing Tracking Services

The ways in which organizations might want to capture information about the execution of their workflows are virtually limitless. So it’s pretty exciting that the tracking infrastructure is so easily extensible. You could create tracking services to write XML to the filesystem or messages to an event log, or to call web services. Creating a new tracking service is relatively simple. The process involves creating two new classes that inherit from TrackingService and TrackingChannel.

The TrackingService class is what is actually added to the runtime with the AddService method and defines how profiles are used as well as how to access the TrackingChannel-associated class that knows how to track data. TrackingService has two overloads to the GetProfile method. The first receives a workflow instance ID and returns its corresponding profile. The second GetProfile overload receives a Type instance pointing to a workflow type as well as the version of the profile to get. A similar method, TryGetProfile, passes a profile back to the called method using an out parameter and returns a Boolean value to indicate whether the profile load was successful. Finally, inherited TrackingService classes must implement the GetTrackingChannel class, which returns the tracking channel to be used with the tracking service.

The TrackingChannel class is what does most of the work related to tracking itself. The Instance CompletedOrTerminated method is called when a workflow instance completes execution normally or is terminated. The implemented code is responsible for tracking this event. The meat of this class is the Send method, which receives a TrackingRecord object.

TrackingRecord is an abstract class; therefore, the instance passed to the Send method is actually one of three inherited classes: ActivityTrackingRecord, UserTrackingRecord, or WorkflowTracking Record. These classes contain useful information such as the date and time the event occurred, the type of object that caused the event to occur, and copies of the data captured by the tracking infrastructure.

The following is an example of a tracking service and corresponding channel that sends events by e-mail. Although tracking workflows using e-mail might not be entirely practical, situations where you want to watch for only workflow failures might be realistic.

  public class EmailTrackingService : TrackingService {     // TrackingService methods     protected override TrackingProfile GetProfile(Guid workflowInstanceId)     {         return this.GetDefaultProfile();     }     protected override TrackingProfile GetProfile(Type workflowType,         Version profileVersionId)     {         return this.GetDefaultProfile();     }     protected override TrackingChannel GetTrackingChannel(         TrackingParameters parameters)     {         return new EmailTrackingChannel("someone@here.com",             "someoneelse@there.com",             "smtp.here.com");     }     protected override bool TryGetProfile(Type workflowType,         out TrackingProfile profile)     {         try         {             profile = this.GetDefaultProfile();             return true;         }         catch         {             profile = null;             return false;         }     }     protected override bool TryReloadProfile(Type workflowType,         Guid workflowInstanceId, out TrackingProfile profile)     {          // setting the output profile to null and returning false tells         // the runtime that there is no new profile(s) to load         profile = null;         return false;    }     // Helper methods     private TrackingProfile GetDefaultProfile()     {         // create a new profile and give it a version         TrackingProfile profile = new TrackingProfile();         profile.Version = new Version(1, 0, 0, 0);         ActivityTrackPoint activityTrackPoint = new ActivityTrackPoint();         ActivityTrackingLocation activityTrackingLocation =             new ActivityTrackingLocation(typeof(Activity));         // setting this value to true will match all classes that are in         // the Activity class' inheritance tree         activityTrackingLocation.MatchDerivedTypes = true;         // add all of the activity execution status as         // something we are interested in         foreach (ActivityExecutionStatus aes in             Enum.GetValues(typeof(ActivityExecutionStatus)))         {             activityTrackingLocation.ExecutionStatusEvents.Add(aes);         }         activityTrackPoint.MatchingLocations.Add(activityTrackingLocation);         profile.ActivityTrackPoints.Add(activityTrackPoint);         WorkflowTrackPoint workflowTrackPoint = new WorkflowTrackPoint();         WorkflowTrackingLocation workflowTrackingLocation =             new WorkflowTrackingLocation();         // add all of the tracking workflow events         // as something we are interested in         foreach (TrackingWorkflowEvent twe in             Enum.GetValues(typeof(TrackingWorkflowEvent)))         {             workflowTrackingLocation.Events.Add(twe);         }         workflowTrackPoint.MatchingLocation = workflowTrackingLocation;         profile.WorkflowTrackPoints.Add(workflowTrackPoint);         return profile;     } } 

The EmailTrackingService class inherits from TrackingService and contains logic to create a default profile that is interested in basically all activity and workflow events. The profile is defined in the GetDefaultProfile method, which says you are interested in everything related to activity and workflow events. Also notice that the GetTrackingChannel method creates and returns an instance of EmailTrackingChannel.

The following is the EmailTrackingChannel class, which inherits from TrackingChannel. As you can see in the Send method, there is logic to check for which type of record you are receiving. At that point, a specific private method is called to track the record in a custom manner. Each custom tracking method eventually calls the utility SendEmail method and passes along relevant data.

  internal class EmailTrackingChannel : TrackingChannel {     private string from;     private string to;     private bool notifyOnCompletionOrTermination;     private SmtpClient smtpClient;     // TrackingChannel methods     public EmailTrackingChannel(string from, string to, string smtpServer)         : this(from, to, smtpServer, false)     {     }     public EmailTrackingChannel(string from, string to, string smtpServer,         bool notifyOnCompletionOrTermination)     {         this.from = from;         this.to = to;         this.notifyOnCompletionOrTermination = notifyOnCompletionOrTermination;         smtpClient = new SmtpClient(smtpServer);     }     protected override void InstanceCompletedOrTerminated()     {         if (this.notifyOnCompletionOrTermination)         {             this.SendEmail("Workflow Done",                 "A workflow instance was completed or terminated.");         }     }     protected override void Send(TrackingRecord record)     {         // check what kind of record we received         if (record is WorkflowTrackingRecord)         {             SendWorkflowData((WorkflowTrackingRecord)record);         }         else if (record is ActivityTrackingRecord)         {             SendActivityData((ActivityTrackingRecord)record);         }         else if (record is UserTrackingRecord)         {             SendUserData((UserTrackingRecord)record);         }         else         {             // we don't know what it is...         }     }     private void SendWorkflowData(WorkflowTrackingRecord workflowTrackingRecord)     {         SendEmail("Workflow event",             workflowTrackingRecord.TrackingWorkflowEvent.ToString() +             " happened at " + workflowTrackingRecord.EventDateTime.ToString());     }     private void SendActivityData(ActivityTrackingRecord activityTrackingRecord)     {         SendEmail("Activity event",             activityTrackingRecord.ActivityType.FullName +             " had execution status " +             activityTrackingRecord.ExecutionStatus.ToString() +             " at " + activityTrackingRecord.EventDateTime.ToString());     }     private void SendUserData(UserTrackingRecord userTrackingRecord)     {         SendEmail("User event",             "The user data was: " + userTrackingRecord.UserData.ToString() +             " at " + userTrackingRecord.EventDateTime.ToString());     }     // Helper methods     private void SendEmail(string subject, string body)     {         this.smtpClient.Send(this.from, this.to, subject, body);     } } 

This overridden Send method of the class inspects the TrackingRecord parameter it receives and conditionally sends some information about the event by e-mail. The method that is called depends on whether the TrackingRecord instance is a WorkflowTrackingRecord, an ActivityTrackingRecord, or a UserTrackingRecord. Each utility method sends specific information about each record type.

Developing Workflow Loader Services

If the DefaultWorkflowLoaderService class functionality is not sufficient (perhaps because you are not using standard workflow definition markup or have your own workflow markup), you can create your own loader service. To do this, you inherit from the abstract WorkflowLoaderService class.

This class contains two overloaded methods that must be overridden in any inherited classes. The first overload of CreateInstance receives a Type reference of the class that represents a workflow definition. The second CreateInstance overload takes two XmlReader instances - the first representing the workflow definition and the second representing that workflow’s rules.

To create a custom workflow loader class, you must effectively implement the behavior of both of these methods and return an Activity reference that represents the top level of the workflow activity tree - meaning the workflow itself. The following is a skeleton of what a custom workflow loader service looks like:

  public class CustomWorkflowLoaderService : WorkflowLoaderService {     protected override Activity CreateInstance(XmlReader workflowDefinitionReader,         XmlReader rulesReader)     {         Activity workflow = null;         // parse the workflow definition         ...         // parse the workflow rules         ...         return workflow;     }     protected override Activity CreateInstance(Type workflowType)     {         Activity workflow = null;         // use the type reference to create the activity tree         ...         return workflow;     } } 

Developing Other Service Types

The runtime service types discussed so far are by no means the only types that can exist in the Windows Workflow Foundation runtime. You can develop and add any type of behavior that can interact with the runtime. This is why the WorkflowRuntime.AddService method takes an object. Services that fall outside the category of extended out-of-the-box services do not follow any particular architecture, so you can develop them to perform just about any task.

Although there are no required interfaces to implement or classes to inherit from, you can enhance runtime services by inheriting from the System.Workflow.Runtime.Hosting.RuntimeService class. This abstract class exposes the base behavior for services that can be started and stopped by the workflow runtime. You can implement initialization and cleanup code in the OnStarted and OnStopped methods. These methods are called when the Started and Stopped events are raised by the runtime, respectively. Finally, classes inheriting from RuntimeService can call the RaiseServicesExceptionNotHandledEvent method when there is an error that is not to be handled by the runtime service itself.

The following code is a skeleton of what a custom workflow runtime service class looks like:

  public class CustomRuntimeService : WorkflowRuntimeService {     protected override void OnStarted()     {         // put some logic here for when the service starts         base.OnStarted();     }     protected override void OnStopped()     {         // put some logic here for when the service stops         base.OnStopped();     } } 



Professional Windows Workflow Foundation
Professional Windows Workflow Foundation
ISBN: 0470053860
EAN: 2147483647
Year: 2004
Pages: 118
Authors: Todd Kitta

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net