Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / SQL-Server

Enhanced Document workflow approval v2

3.86/5 (5 votes)
24 Mar 200713 min read 1   1.2K  
This Article supporting these features: Logging to file, TransactionScopeActivity, WorkflowCommitBatchService, SqlTrackingService, fault Handler Activity and Bi-Directional communication using custom services

Image 1

The purpose of the article

Demonstrating these extra features

• Moving all interfaces in 1 assembly and all other projects will reference this assembly.

• Registering the services "out of the box and custom services" in application configuration file for better maintenance.

• Using "out of the box" system diagnostics LogToFile utility .

• Using "out of the box" WorkflowCommitBatchService, internally with using TransactionScopeActivity

and preparing our document service to work with batches and transactions.

• Using Fault Handler Activities to handle workflow internal errors.

• Bi-Directional communication, workflow with the host, through the custom service.

• Using "out of the box" SqlTrackingService, for tracking the workflow

• Using SqlTrackingService utility for query the tracked workflow.

If you did not read, the first article Document approval workflow system

I encourage you to read the article, and to run it, because this article is enhanced version for better design,

and for more workflow services.

To run this Application

• You should install .Net 3 released version

http://www.microsoft.com/downloads/details.aspx?familyid=10CC340B-F857-4A14-83F5-25634C3BF043&displaylang=en

• you should have Visual studio extension for windows workflow

http://www.microsoft.com/downloads/details.aspx?familyid=e8232f93-48f0-4e74-b09d-b51f1d4231a4&displaylang=en

• This version of the application using SQL Server 2000

• You should create a database for SQL persistence service and for SQL Tracking service These scripts, under

C:\$windows folder$\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN

• For SQLPersistenceService you may need to run the Distributed Transaction Coordinator service

from control panel -> Administrative tools->services choose Distributed Transaction Coordinator service and run it if it is not running

• For transaction service you should to configure your component service,

from control panel -> Administrative tools->Component service then choose computers -> MyComputer

then with the context menu choose properties and in default properties tab tick Enable distributed com

on this computer and from MSDTC tab choose tick use local coordinator If you run any workflow

samples using SQLPersistenceService with TransactionScopeActivity so you have no need to

do any of the previous settings , and you may to do only the next settings

• You should create the application database that is in download source file

• Then change the app.config file to point to the new name of database or leave as it is if you choose

workflowDB the default database name for persistence and tracking database and DocumentDB for application database

• You may need to populate the Document database with some data to

initialize the users and groups that will appear in Client and Administration application,

so read the readme.txt in the package (zip file)

Features in more detail

• Moving all interfaces in 1 assembly and all other projects will reference this assembly.

This topic was mentioned in detail in the first article,

the Idea is simple, try to put your workflow and all workflow helper classes in separate project,

then separate all interfaces that your workflow need in separate project ,

finally write all your core application code to implement and to use the interfaces,

not the concrete classes so, if you did that

you will achieve 2 main benefits
• You will avoid circular reference, because the workflow will use services in the core application,

and the core application will use the workflow, so the compiler will not allow you to do that.

• General concept for programming using the interfaces makes your application more dynamic and support versioning.

Note, some cases you may use abstracted class rather than interface, in some situations using the

abstracted classes for your basic library is much better than interface, it is out of this article scope or

always use abstracted classes then wrap it with interfaces in separate assembly

"I do that very often" to be used in client classes,

But the concept is the same "the abstracted classes and interfaces should always be away

from the core library or business logic projects".

Figure 1 DocumentBaseLibrary has all application interfaces and abstracted classes and enumerable data types

Image 2

Figure 2 Class diagram of interfaces in DocumentBaseLibrary

Image 3

Figure 3 abstracted classes in DocumentBaseLibrary

Image 4

Registering the services "out of the box and custom services" in application configuration



for better maintenance, You may register the workflow runtime in the app.config and name this section with any name and user the

Workflow Runtime constructor to use it App.config sample
<configuration>
  <configSections>
    <section
       name="WfRuntime"
       type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection,
            System.Workflow.Runtime, Version=3.0.00000.0,
            Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
  </configSections>
  <connectionStrings>
    <add name="documentDB"
        connectionString="Data Source=(local);Initial Catalog=documentDB;Persist Security Info=True;User ID=sa;Password=sa"
        providerName="System.Data.SqlClient" />
    <add name="workflowDB"
        connectionString="Data Source=(local);Initial Catalog=workflowDB;Persist Security Info=True;User ID=sa;Password=sa"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.diagnostics>
        <switches>
            <add name="System.Workflow.Runtime" value="All" />
            <add name="System.Workflow.Runtime.Hosting" value="All" />
            <add name="System.Workflow.Runtime.Tracking" value="Critical" />
            <add name="System.Workflow.Activities" value="Warning" />
            <add name="System.Workflow.Activities.Rules" value="Off" />

            <add name="System.Workflow LogToFile" value="1" />
        </switches>
    </system.diagnostics>
  <WfRuntime>
    <CommonParameters>
      <add name="ConnectionString"
           value="Data Source=(local);Initial Catalog=WorkflowDB;
                     Integrated Security=true"/>
    </CommonParameters>
    <Services>
      
      <add type=
       "System.Workflow.Runtime.Hosting.DefaultWorkflowSchedulerService,
         System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
         PublicKeyToken=31bf3856ad364e35"
        maxSimultaneousWorkflows="3" />
      <add
        type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService,
              System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
              PublicKeyToken=31bf3856ad364e35" UnloadOnIdle="true" />
      <add type=
       "DocumentCore.Bll.DocumentTransactionalService,
         DocumentCore"/>
      
        <add
          type="System.Workflow.Runtime.Tracking.SqlTrackingService,
              System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
              PublicKeyToken=31bf3856ad364e35"
              
             />
      
    </Services>
  </WfRuntime>
</configuration>

It is good Idea to create a WorkflowHelper Class that is singleton or static class has only static members and has different

WorkflowRuntime properties to simplify the workflow runtime initialization and to avoid running more

than workflow runtime instance, or to load the workflow from different configuration sections

This class should encapsulate all accessing to workflow sections, and responsible to initiate the workflow instance

Using "out of the box" system diagnostics LogToFile .

LogToFlile is configurable diagnosis utility, under system.diagnostics section in app.config

After you run the application you will find a log file created called WorkflowTrace.log

beside the text file, and that is the sample of this file result

System.Workflow.Runtime Start: 0 :
Workflow Runtime tracer is alive! System.Workflow.Runtime.Hosting Information: 0 :
WorkflowRuntime: Created WorkflowRuntime 7816a986-75e8-4383-81dc-033c17ce8055 System.Workflow.Runtime.Hosting Information: 0 : WorkflowRuntime: Starting WorkflowRuntime 7816a986-75e8-4383-81dc-033c17ce8055 System.Workflow.Runtime.Hosting Information: 0 : SqlWorkflowPersistenceService(00000000-0000-0000-0000-000000000000): Starting, LoadInternalSeconds=120 System.Workflow.Runtime.Hosting Information: 0 :
DefaultWorkflowCommitWorkBatchService: Starting System.Workflow.Runtime.Hosting Information: 0 : SqlWorkflowPersistenceService OpenConnection start: 03/20/2007 19:09:19 System.Workflow.Runtime.Hosting Information: 0 : SqlWorkflowPersistenceService. OpenConnection end: 03/20/2007 19:09:20 System.Workflow.Runtime.Hosting Information: 0 : SqlWorkflowPersistenceService.RetrieveNonblockingInstanceStateIds ExecuteReader start: 03/20/2007 19:09:20 It is not fair to compare workflow logging utilities and workflow tracking services The Logging utility will help you in programming time to debug the workflow (the track service will do that as well) the result will be written in plain file, so it will be quite hard to extract any information , or to run queries against this file, and this file should be very large. You should purge this file regularly So logging is not reliable neither customizable like tracking service, so It is good Idea if you enabled the Logging utility in debugging mode only, and stop it in release mode using #if DEBUG directive

Using "out of the box" WorkflowCommitBatchService

using this service internally using TransactionScopeActivity and preparing our document service

to work with batches and transactions.

Working with transaction in workflow is something very important, it is essential especially in real world workflow scenarios,

if any step failed and all the dependent steps not rolled back you will suffer the data inconsistency in your application.

Few programmers design their workflows and finish the applications, then after they finish the basic test

they begin adding transactional services and fault handle activities.


It is not bad idea as long as your application will support the transaction and has a well "Disaster Recovery Plan"

before the application deployed in production server.

Now, the problem of transaction came to our workflow because the fact of using more than 1 database

in workflow applications at least 1 database for the SQLPersitanceService

and at another (or more) for the application itself ( by the way you can let the tracking service participate in transaction

as well and may be had another database) So the Work flow foundation gives you a very handy tool

for to manage transaction without explicitly using transaction object, or distributed transaction service,

simply you will drag the TransactionScopeActivity and put your activities inside it.

As long as you will not put any activity make the workflow idle "like long delay activities or listener activities"

And remember that you could not make the transaction opening for such long time.

Then your custom service should implement IPendingWork
public interface IPendingWork
    {
        void Commit(Transaction transaction, ICollection items);
        void Complete(bool succeeded, ICollection items);
        bool MustCommit(ICollection items);
    }
Then all your method should do nothing but save all application request information (method name, parameters)

to Workbatch object and in the Commit method you will perform the real actions Note that if you are working with

SQL Server 2000, by default all your database connection will be Enlisted the global transaction,

so do not use connection.EnlistTransaction() method,

it will through exception because it should belongs

to one transaction only, if you want to check the current global transaction simply put break point in any

method and test this command in Immediate window System.Transactions.Transaction.Current

Just to be confident that your own code is working in transaction

Now that is sample of Document TransactionalService code
[Serializable]
    public class DocumentTransactionalService : IDocumentService, IPendingWork
….
public void CreateInWorkflowDocumentForUser(IDocument document, string approvalUser, Guid workflowID)
        {
            Request req = new Request();
            req.RequestType = RequestType.CreateInWorkflowDocumentForUser;
            req.Document = document;
            req.ApprovalUser = approvalUser;
            req.WorkflowID = workflowID;
            WorkflowEnvironment.WorkBatch.Add(this, req);
        }
public void ApproveDocumentWorkflow(IDocument document, string approvalUser, Guid workflowID)
        {
            Request req = new Request();
            req.RequestType = RequestType.ApproveDocumentWorkflow;
            req.Document = document;
            req.ApprovalUser = approvalUser;
            req.WorkflowID = workflowID;
            WorkflowEnvironment.WorkBatch.Add(this, req);

        }
public void Commit(System.Transactions.Transaction transaction, System.Collections.ICollection items)
        {
            foreach (Request request in items)
            {
                switch (request.RequestType)
                {
                    case RequestType.ApproveDocumentWorkflow:
                        ApproveDocumentWorkflowInTransaction(request.Document, request.ApprovalUser, request.WorkflowID);
                        break;
                    case RequestType.CreateInWorkflowDocumentForGroup:
                        CreateInWorkflowDocumentForGroupInTransaction(request.Document, request.ApprovalGroup, request.WorkflowID);
                        break;
                    case RequestType.CreateInWorkflowDocumentForUser:
                        CreateInWorkflowDocumentForUserInTransaction(request.Document, request.ApprovalUser, request.WorkflowID);
                        break;
                    case RequestType.RejectDocumentWorkflow:
                        RejectDocumentWorkflowInTransaction(request.Document, request.ApprovalUser, request.WorkflowID);
                        break;
                    default: throw new NotSupportedException(request.RequestType.ToString());
                }
            }
        }

        public void Complete(bool succeeded, System.Collections.ICollection items)
        {
            // nothing to clean
        }

        public bool MustCommit(System.Collections.ICollection items)
        {
            return true;
        }

Testing the Transactional service: The simplest way to proof that your workflow is transactional,

simply throw an unhandled exception after any activity that made changes in database ,

and double check this exception with registering WorkflowTerminated event of workflowRuntime.

Then you will see that all changes rolled back again.

In our sample the Purchase branch has unhandled throw activity, this activity throw an Exception of the type

System.InvalidOperationException, although I put fault handler to handle this error but I re threw it again,

Then you will see how the document will be rolled back to open state despite the changes that happened

with the first activity that kicked him in workflow in purchase group.


Figure 4 Throw activity1 will halt the workflow however the workflow will

rollback all works that be made through the activity in ApproveFromPurchaseGroup branch
Image 5

Using Fault Handler Activities to handle workflow internal errors.

Fault handler is exactly catching and handling exception in simple c# application,

the only advanced features in the workflow designer simply is the fault handler designer, otherwise is the same.

So whatever you know in catching exception is c# code you can apply it in fault handling,

and that is the simple guidelines to use fault handlers

• Never catch exception without good handling plan, it is better to leave it and the host will handle it

or will leave it as unhandled exception

• Avoid Catching generic Exception like System.Exception always use multiple catch blocks,

and let it be ordered from the most specific exception to the most generic exception Like this code
try
            {
            }
            catch (OperationCanceledException oEx)
            {
                // code to handle this specific exception
            }
            catch (NotSupportedException nEx)
            {
                // code to handle this specific exception
            }
            // ---
            // ---
            catch (Exception ex)
            { 
            // generic exception that all code will come here if it is not fell down in any other exceptions
}

There are many other design guidelines in exceptions like wrapping exceptions with custom exceptions

and log the tech. Exceptions and add the friendly message to application exception,

all these detail is out of this article scope.

Now you can map the workflow fault handlers with the previous Exception code

First you should select the fault handler designer like in figure 5.

Figure 5 fault handler designer

Image 6

Note that not all activities support fault handlers, only Condition branches and other activities

support fault handlers, Then you should drag the fault handler activity in the fault handler design

(you can add more than default handler, each one can handle "catch'" different exceptions and select from the story board one activity handler)

Figure 6

you can choose 1 FaultHandlerActivity from story board, and add your activity to handle this specific exception (fault) Image 7

Second step you should choose the Exception that your handler will handle from FaultHndlerActivity

Figure 7 FaltType is representing a specific Exception Type
Image 8

If you choose System.Exception you will handle ALL Exceptions but it is not

good Idea at all, you should as possible put handlers for specific faults.

Transaction and fault handling:

If you are in transaction scope and you caught the exception and did not threw it again,

the transaction will commit partially so, you will end up with in consistence data So be careful with handling

exceptions, in case of transactions. In the code sample, I made up 2 fault branches and I emulated the error through

throw exceptions, and both under global transaction, one of them was handled and re threw again, and the other was handled and

absorbed, so in the second scenario we ended up with data inconsistency,

so you should roll back the transaction or throw it again

Bi-Directional communication, workflow with the host, through the custom service.

The workflow should communicate with the host with any way, but we know there are basic of steps

that we expect if an Object A will communicate with an Object B.

First we have to create a channel or queue to pass (Queue) the data,

Then we have to notify the client that the data is ready or available One of the communication

techniques is using WorkflowQueuingService, it is ready to use built in services, But I will focus in this topic about

the custom service, and how you can use it as Bi-direction communication You can let the client Notify the workflow

with any change and sending the data that the workflow wants And you can let the workflow notify

the client with any change and supplying the client with any data that he wants

Let us examine the both scenarios:


• Notify the workflow with changing from the Client:
You can do that with using DataExchangeService , simply decorate your Service interface with this attribute [ExternalDataExchange] Like in
the sample below
[ExternalDataExchange]
    public interface IDocumentService
    {

        #region events to fire methods for workflow
        event EventHandler<externalexternaldocumenteventargument __designer:dtid="562949953421586"> RaiseApproveDocumentWorkflowEvent;
        event EventHandler<externalexternaldocumenteventargument> RaiseRejectDocumentWorkflowEvent;
        
        #endregion
/// other code
  }
</externalexternaldocumenteventargument></externalexternaldocumenteventargument>

And use special events that Generic with classes inherits from ExternalDataEventArgs
[Serializable]
    public class ExternalExternalDocumentEventArgument:ExternalDataEventArgs
    {
/// other code
}

Then you may add ListenActivity in your workflow and waiting for the client to raise event,

you have to register the workflow for these specific events through HandleExternalEventActivity

Figure 8 Listener
Image 9

Each HandleExternalEventActivity, will use one event that has a ExternalDataEventArgs class,

for any interface decorated with [ExternalDataExchange]

• Notify the client with changing from the workflow:

The beauty of the workflow service that you can access this service any where as long as you can access

WorkflowRuntime Class so you can use the GetService<>() method to get a reference for the service,

then you will use the service like any object that you already used to. For example,

I created method and events that will be raised internally in the Classes that will implement IDocumentService
[ExternalDataExchange]
    public interface IDocumentService
    {

// code

        #region events to used in the host, to track what is happening in workflow
        event EventHandler<documenteventarguments __designer:dtid="562949953421612"> OnCreated;
        event EventHandler<documenteventarguments> OnUpdated;
        event EventHandler<documenteventarguments> OnArchived;
        event EventHandler<documenteventarguments> OnDeleted;
        event EventHandler<documenteventarguments> OnSentToWorkflow;
        event EventHandler<documenteventarguments> OnApproved;
        event EventHandler<documenteventarguments> OnRejected;
        event EventHandler<documenteventarguments> OnError;
        #endregion
</documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments>

Then I raised this events inside DocumentTransactionalService protected void
CreateInWorkflowDocumentForUserInTransaction(IDocument document, string approvalUser, Guid workflowID)
        {
            DocumentDalc.CreateInWorkflowDocumentRecordForUser(document, workflowID, approvalUser);
            if (this.OnSentToWorkflow != null) OnSentToWorkflow.Invoke(this, new DocumentEventArguments(document, "InWorkflow"));

        }
So you can let the client register this event to notify him, when the document will be sent to workflow,

So, you can simply create events as much as you can in the custom services, and make the client be

notified with any change May be you need to pass the workflow id for tracking workflow instance purpose,

you can custom your event argument as you like
_service = WorkflowHelper.DocumentService as DocumentTransactionalService;
            _service.OnCreated += ReceiverEvents.ServiceEvents;
            _service.OnSentToWorkflow += ReceiverEvents.ServiceEvents;
            _service.OnError += new EventHandler<documenteventarguments __designer:dtid="562949953421622">(_service_OnError);
            return wr;
</documenteventarguments>

Using "out of the box" SqlTrackingService, for tracking the workflow

Tracking service is one of the most important services in workflow, that is for simple reason that the workflow state is

saved as binary data in database, and after the workflow instance completed , it will be removed from InstanceState

table in database, and there is no logging or datahistory for InstanceState data in the database So, you need to

track the workflow events, before and after workflow instance be completed. You can use "out of the box" ,

SqlTrackingService And it is ready to use, just create the database " you may choose the same workflowDB as in the

sample" You can use profile to track specific events, or leave it with default profile, so you will track all events You

can make your tracking service became transactional The target of this article is giving you a very quick look to

tracking service, through registering the SqlTrackingService in app.config file


<add
          type="System.Workflow.Runtime.Tracking.SqlTrackingService,
                  System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
                  PublicKeyToken=31bf3856ad364e35"
              
             />
This service is using SQL Server database, by default it will use the database in common parameter inside the WorkflowRuntime section
<CommonParameters>
          <add name="ConnectionString"
           value="Data Source=(local);Initial Catalog=WorkflowDB;
                     Integrated Security=true"/>
</CommonParameters>


The tracking service basically track 3 main events

• User Events.

• Workflow Events.

• Activity Events.

It is, obvious that the database will grow very quick not like the persistence service that purge each workflow

after each workflow ended. This problem was solved through Table partitioning property In tracking service,

You can configure the tracking service to be partitioned every month or year or any period that you wish.

In the application sample I used the minimum SQLTrackingService configuration, just to run the Queries

against the tracking service and to taste the beauty of this service.

Now to run the Tracking service reports, first run the Administration application and choose from the menu

That is the code for running the Tracking queries
private IList<UserTrackingRecord> GetUserEvents(Guid instanceID)
    {
        SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(WorkflowHelper.TrackingConnectionString);
        SqlTrackingQueryOptions ops = new SqlTrackingQueryOptions();
        SqlTrackingWorkflowInstance wftrackInstance;

        sqlTrackingQuery.TryGetWorkflow(instanceID,out wftrackInstance);
        return wftrackInstance.UserEvents;
    }
    private IList<ActivityTrackingRecord> GetActivityEvents(Guid instanceID)
    {
        SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(WorkflowHelper.TrackingConnectionString);
        SqlTrackingQueryOptions ops = new SqlTrackingQueryOptions();
        SqlTrackingWorkflowInstance wftrackInstance;

        sqlTrackingQuery.TryGetWorkflow(instanceID, out wftrackInstance);
        return wftrackInstance.ActivityEvents;
    }
    private IList<WorkflowTrackingRecord> GetWorkflowEvents(Guid instanceID)
    {
        SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(WorkflowHelper.TrackingConnectionString);
        SqlTrackingQueryOptions ops = new SqlTrackingQueryOptions();
        SqlTrackingWorkflowInstance wftrackInstance;

        sqlTrackingQuery.TryGetWorkflow(instanceID, out wftrackInstance);
        return wftrackInstance.WorkflowEvents;
    }

Conclusion

To work with windows workflow foundation, in real application, You can use a lot of "out of the box"

Services that be introduced by Windows workflow foundation frame work To take off the burden of coding

the transactional database commands and tracking Database, or other useful services

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here