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
Figure 2 Class diagram of interfaces in
DocumentBaseLibrary
Figure 3 abstracted classes in
DocumentBaseLibrary
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)
{
}
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
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)
{
}
catch (NotSupportedException nEx)
{
}
catch (Exception ex)
{
}
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
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)
Second step you should choose the Exception that your handler will handle from
FaultHndlerActivity
Figure 7 FaltType is representing a specific
Exception Type
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
}
</externalexternaldocumenteventargument></externalexternaldocumenteventargument>
And use special events that
Generic with classes inherits from ExternalDataEventArgs
[Serializable]
public class ExternalExternalDocumentEventArgument:ExternalDataEventArgs
{
}
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
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
{
#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