Transactional Services


Let's summarize a few things we have learned in this chapter:

  • WF program instance persistence occurs at the completion of a transactionScopeActivity or any activity whose type carries PersistOnCloseAttribute.

  • Persistence can also occur at arbitrary times, at the discretion of the host application, as part of WF program instance passivation (WorkflowInstance.Unload).

  • In failure scenarios (or after WorkflowInstance.Abort is called by the host application), WF program instances resume their execution from their last persistence point.

As we have confirmed throughout the book, one of the distinguishing characteristics of WF programs is that they execute episodically. These episodes generally correspond to computation that is triggered by the arrival of data and ends when the WF program instance becomes idle (waiting for additional stimulus).

We can take a slightly different angle and alternatively classify an episode as the computation that takes place between two persistence points of a WF program instance. This is a slightly different definition (because this kind of episode can span idle points if a host application chooses not to persist when an instance becomes idle) but is arguably more precise. It is more precise because an episode cannot really be said to have "happened" until a persistence point is reached and the progress of the WF program instance is recorded in a transactional manner.

Let's explore this further. It is beneficial to use activities such as Update-CustomerInfo within a transactionScopeActivity because the types they rely upon in their execution logic (such as the types defined in the System.Data.SqlClient namespace) know how to enlist in the ambient transaction. The work of these activities occurs transactionally, and we have confirmed that the WF program instance is also always persisted as a part of the same transaction. So far, so good.

What about nontransactional work (like our WriteLine activity) that prints a message to the System.Console (which, alas, is not a System.Transactions resource manager)? Maybe it is all right for a message to be printed to the console more than once in the failure case we outlined earlier (wherein the same WriteLine activity might have its Execute method called more than once). In some scenarios, though, we may need to do betterfor example, if we want the message to be printed exactly once.

Although a WriteLine activity may have its Execute method invoked more than once (as in the failure case), the execution of that WriteLine nevertheless must be said to "happen" exactly once if our source of truth is the data (WF program instance state) that is sent to the persistence service at persistence points. That is to say, a transaction is created (or borrowed from transactionScopeActivity) when a persistence point occurs, and only when this transaction commits is a WriteLine activity executed (as part of the current episode), recorded, and said to have verifiably "happened."

The key insight here is that if we can delay the actual invocation of Console.WriteLine until a transaction (for the next persistence point) is available, the occurrence of the actual side effect (seeing a message appear at the console) and the WF runtime's recording of the completion of the program statement that caused this side effect (the completion of the WriteLine activity) will happen in the same transaction!

We can, in fact, achieve this splendid result quite simply. In Chapter 3, "Activity Execution," we discussed the benefits of factoring execution logic out of activities and locating it in services that are added to the WF runtime. We already did this factoring very straightforwardly for WriteLine back in Chapter 3:

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public abstract class WriterService   {     public abstract void Write(string s);   }   public class WriteLine : Activity   {     // Text property elided for clarity     protected override ActivityExecutionStatus Execute(       ActivityExecutionContext context)     {       WriterService writer = context.GetService<WriterService>();       writer.Write(Text);       return ActivityExecutionStatus.Closed;     }   } } 


Our first implementation of a WriterService was very simple:

 using System; using EssentialWF.Activities; namespace EssentialWF.Services {   public class SimpleWriterService : WriterService   {     public override void Write(string s)     {       Console.WriteLine(s);     } } } 


The SimpleWriterService implementation does buy us a layer of abstraction between the WriteLine activity and the actual assets used to write a message (which is most definitely a good design practice), but it of course does not change the fact that the appearance of the message at the console is not occurring under the auspices of any transaction.

However, we can also implement a transaction-aware WriterService derivative that delays the call to Console.WriteLine until a transaction (associated with a persistence point in the WF program instance) is provided by the WF runtime. We do this by implementing the System.Workflow.Runtime.IPendingWork interface.

The IPendingWork type is shown in Listing 6.4.

Listing 6.4. IPendingWork

 namespace System.Workflow.Runtime {   public interface IPendingWork   {     void Commit(System.Transactions.Transaction transaction,       ICollection items);     /* *** other members *** */   } } 


A service that implements IPendingWork indicates that it is capable of processing work items in a transactional manner. Work items are handed to such a service (via invocation of the IPendingWork.Commit method) from what is known as a work batch. A work batch holds a set of work items for a specific WF program instance.

The work batch for the currently executing WF program instance can be obtained via the WorkBatch property of the WorkflowEnvironment type. WorkflowEnvironment is a static class defined in the System.Workflow.Runtime namespace, and is shown in Listing 6.5.

Listing 6.5. WorkflowEnvironment

 namespace System.Workflow.Runtime {   public static class WorkflowEnvironment   {     public static IWorkBatch WorkBatch { get; }     public static Guid WorkflowInstanceId { get; }   } } 


A work batch is represented by an object of type IWorkBatch, which is defined in the System.Workflow.Runtime namespace and shown in Listing 6.6.

Listing 6.6. IWorkBatch

 namespace System.Workflow.Runtime {   public interface IWorkBatch   {     void Add(IPendingWork service, object workItem);   } } 


As you can see in Listing 6.6, when a work item (of type object) is added to a work batch, it is associated with a service (which must implement IPendingWork) that will later process the work item (when a transaction is available). When items in a work batch are processedas part of the transaction that occurs at a WF program instance persistence pointeach work item is handed to its associated service, along with the actual System.Transactions.Transaction that should be used in the performance of the work.

Here is our transaction-aware WriterService implementation:

 using System; using System.Collections; using System.Transactions; using System.Workflow.Runtime; using EssentialWF.Activities; namespace EssentialWF.Services {   public class TransactionalWriterService :     WriterService, IPendingWork   {     public override void Write(string s)     {       IWorkBatch batch = WorkflowEnvironment.WorkBatch;       batch.Add(this, s);     }     public void Commit(Transaction transaction, ICollection items)     {       foreach (object item in items)       {         string s = item as string;         Console.WriteLine(s);       }     }     // Called after the transaction succeeds or fails     public void Complete(bool succeeded, ICollection items)     {     }     // Called to see if Commit should be called     public bool MustCommit(ICollection items)     {       return true;     }   } } 


In its implementation of the Write method, transactionalWriterService adds a work item to the work batch of the currently executing WF program instance. The work item is simply the string passed as a parameter to Write. At some point in the future, when a transaction is available at a WF program instance persistence point, the IPendingWork.Commit method is called by the WF runtime. At this time, transactionalWriterServiceImpl simply prints each work item (remember, in this case, each work item is a string that was passed earlier to the Write method, which could have been called more than one time if WriteLine executed more than oncefor instance, inside a While activity).

All we have done is delay the execution of the invocation of Console.WriteLine. This may seem trivial (and the example is definitely contrived), but the benefit of having the actual work specified by an activity performed and committed in the same transaction that the WF runtime uses to record the successful execution of that activity is critical in some scenarios.

There is one rather serious consequence to this design. If an exception is thrown by the Commit method of transactionalWriterService, the transaction under which the work (and the persistence of the WF program instance) is happening fails.

For this reason (and perhaps also for reasons of performance or scalability), the implementation of a transaction-aware service that is utilized by an activity might not choose to do the actual work requested by the activity as part of the transaction that occurs at a WF program instance persistence point, but instead can transactionally record the fact that this work must occur. In other words, there can be a transactional handoff of data from the service (and hence, logically, the WF program instance) to another system, using a transactional medium such as a database table or a Microsoft Message Queuing (MSMQ) queue. Unlike our simple example, in this more robust solution the System.Transactions.Transaction object that is passed as a parameter to the IPendingWork.Commit method will be needed by the transaction-aware service to perform the handoff of work.




Essential Windows Workflow Foundation
Essential Windows Workflow Foundation
ISBN: 0321399838
EAN: 2147483647
Year: 2006
Pages: 97

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