The Routing Problem: The Microsoft Exchange Server Solution

Andrew Wallace
Microsoft Corporation

April 1998

Click to view or copy the sample files associated with this technical article.

So you want to send a document to a sequence of destinations for review, and track the progress of the document through the cycle. Or maybe you just want to send a Web page to a group of editors for approval before putting it out on the Web. Or you want to develop a sales automation application, or even just a means of passing expense reports to the appropriate managers for review.

Traditionally, you can either build a sophisticated application on a database engine, which is great if the application justifies the cost. Or you can try using the very simple routing slips that come with Microsoft® Office. The routing slip in Office is an example of client-side routing. This approach suffers from a number of drawbacks. For example, there is no reliable way to ensure execution in a prescribed period of time or to guarantee that execution will complete. It is also difficult to get an overview of the status of such routes.

The only way around these reliability, timing, and administration issues is with a server-side solution. The trick is to do it without incurring a high development cost.

Microsoft Exchange Server Routing Solution

The Microsoft Exchange Server Routing Objects and Routing Engine have been designed to address these issues. The rest of this article describes the Exchange Server routing solution and gives an example of how to use it.

From a high-level or architectural viewpoint the Exchange Server routing solution is a hub-and-spoke architecture. An Exchange Public Folder (or a mailbox) acts as the hub, and the flow of messages to and from each participant in the route appears as the spokes. Figure 1 shows the logical view of a simple route on the left. The diagram on the right shows the actual movement of messages.

Figure 1. Logical view of route compared to the actual movement of the messages

From an implementation perspective the Exchange Server routing solution could be conceived of as a stateful scripting agent. There are four components:

The Routing Engine has been implemented as a custom agent that runs under the control of the Exchange Event Service. It is configured on a per-folder basis and responds to all the events generated by the Event Service on that folder.

The Routing Map is a low-level description of the logic involved in the route. A task such as "Get Manager's Approval" is broken down into its basic logic and functions. An example of such a breakdown would be:

  1. Send Message to Manager

  2. Wait for either a response or for 24 hours

  3. Evaluate if an expiration occurred

  4. If so, branch to timeout handler

  5. Else, we got a message back, so

  6. Open message and collect approval status

  7. Evaluate if the request was approved

  8. If so, branch to approval handling

  9. Else, branch to rejection handling

Logic at this level can be encoded more or less directly into the Routing Map as shown in Table 1.

Table 1. A Map to Get Approval from a Manager

Activity ID Action Argument
10 Send Manager
20 Wait 24 hours
30 ORSplit IsTimeout
40 Goto Timeout
50 Receive  
60 ORSplit IsApprove
70 Goto Approved
80 Goto Rejected

The Routing Map contains two kinds of activity:

Table 2 lists the set of activities supplied with Exchange Server version 5.5 Service Pack 1.

Table 2. Routing Activities with Exchange Server Version 5.5 Service Pack 1

Type Activity Description
Flow Control    
  New Instantiates a new route process instance.
Terminate Terminates a route process instance.
Wait Waits for an event or an expiry timer.
ORSplit Evaluates a VBScript function and skips a line if it returns false. The logic branches one way or the other.
AndSplit Creates a set of child process instances. The logic branches in parallel.
Goto Branches execution to a particular activity.
Evaluation    
  IsTimeout Did this process instance leave its previous state due to expiry?
IsApprove Did the participant approve the process instance?
IsApproveTable Evaluates the complete list of votes.
Functional    
  Send Sends a message.
Receive Updates status and properties when a valid message is received.
Consolidate Moves properties from a work item onto the process instance.
FinalizeReport Generates a summary report on a process instance.
AutoSet Provides default Approval or Rejection behavior for participants.

Note   The Microsoft Exchange Server version 5.5 Service Pack 1 is a forthcoming Service Pack, however, at the time of this writing (April 1998) a beta version of The Microsoft Exchange Server Routing Objects is available, after registration, on the Microsoft TechNet IT Home Web site at http://technet.microsoft.com/reg/download/exchange/default.htm.

Clearly, most users are not going to describe a route at this level. Rather, users will have their choice of design tools or authoring environments that take a higher level description of the route and translate it into a routing map that the routing engine can interpret. The low level of the map allows for a great deal of flexibility in the behavior of the route.

Exchange Server 5.5 Service Pack 1 includes a Routing Wizard, which is an example of a fairly simple authoring tool that can be used to create short sequential and parallel routes. The source code is included on the Service Pack 1 CD. The wizard creates a route and saves it on the folder, such that any message that is dropped or sent to this folder will be routed according to the map.

The sample described below is an even simpler authoring tool that is hosted as a Microsoft Outlook™ form. The form creates a map, saves it onto the message being composed, and posts it into a folder for routing. Observe the distinction between this approach and the Routing Wizard, in that the route defined with this form is applied only to the particular message being created. These are referred to as one-off maps.

The Routing Objects are used to manipulate the map, and process instances. A list of the objects and their most interesting properties and methods are given in Table 3.

Table 3. Exchange Routing Objects

Object Property/Method Description
Routing Map    
  Open/Save Opens and saves routing maps to a folder or message.
Insert/Get Activity Inserts and gets routing activities on and from the map.
Process Instance    
  RUI Routing Unique Identifier. Uniquely identifies a particular process instance.
Open/Save Opens or saves a process instance.
Status The current high level status of process instance.
Timeout The time when the process instance will expire (that is, move to next state).
ParentProcID The ID of the parent process—used when this process instance is a child of another process.
Log Returns the Log property of the process instance.
Work Item    
  Correlate Correlates a work item to its parent process instance.
Consolidate Moves properties from the work item to the process instance.
EmbedMsg Embeds a message on the work item.
Routing Activity    
  ActivityID Identifies the activity in the map.
Action The action to be taken for this activity.
Flags  
Args The arguments to be passed to a function as part of an action.
Routing Participant   The participant in a route. Anything with an email address can be a participant.
  RoleName The name of the participant.
MemberName The address of the participant.
ResolveRole Resolves the role of the participant to an address.
Routing Log   Builds a log for the process instance. Entries in the log can come from the engine, script, or even be made at the client if a form is used.
  Open/Save Opens and saves the log for this process instance.
Get/AddLogEntry Gets and adds log entries.
Routing Voting   Models the properties used to enable the Outlook client's voting buttons and interpret the results.
  PopulateVoteMessage This sets the voting properties on the message.
Item A recipient entry on the voting message.
Count The number of recipient entries on the voting message.
Recipient Entry   Models the vote that a recipient gave on a message.
  Recipient The name of recipient.
Status The status of the recipient's vote.
Date The date of the recipient's last change in status.

Some of the terminology being used here probably bears explanation. The four key concepts are described below:

Routing map

Lists all the possible states in a route. It is traversed from top to bottom, much like a program, and branches when indicated by a particular state.

Activity

Defines what happens in the map at a particular state. Examples would be "SendMessage", "Goto X", "Wait", and so on.

Process Instance

Models a particular route and stores all the information for that route. For example, dropping a spreadsheet into a folder will cause a process instance to be created and the spreadsheet to be embedded in it.

Work Item

Any new item in a folder is modeled as a work item until the process instance it relates to is found, or, if none is found, it is used as the basis from which to create a new process instance.

Exchange Server 5.5 Service Pack 1 includes several components for routing. The Routing Engine and the Routing Objects are installed to the server by default if the Exchange Scripting Agent is present. The Routing Wizard is on the CD and can be installed on any client machine. The Routing Wizard can be used to either build the bindings and scripts for the Event Service, or to create some simple approval routes.

The Sample

By way of a sample to demonstrate the use of the Exchange Routing objects, we have created an Outlook form that creates a message, with a one-off map, and drops it in a routing folder. When opened in read mode, the form also shows a tracking tab to enable the user to view the status of the route.

Figure 2 shows a picture of the form in compose mode. The user is able to select the recipients to receive the form, enter subject, and some text. For simplicity, the order in which the recipients receive the form is given by the order in which they are selected from the address book.

Figure 2. The sample form in compose mode

The following code creates a message and instantiates a map object. It also populates the recipient list with resolved names.

    RTMap.Message = cdoMessage
    RTMap.openmap 1

    Set cdoTemp = cdoSession.inbox.messages.Add
    cdoTemp.Recipients.addmultiple txtRecip.Text
    cdoTemp.Recipients.Resolve
    Set Recips = cdoTemp.Recipients
    
    If cdoTemp.Recipients.Resolved = False Then
       For k = 1 To cdoTemp.Recipients.Count
            On Error Resume Next
            cdoTemp.Recipients.Item(k).Resolve True
            On Error GoTo 0
            If Err Then Exit Function
        Next
    End If
    
    MakeMap RTMap, Recip
    RTMap.savemap

MakeMap is the function that does most of the work for the route. First it inserts some preprocessing logic in the map to handle arrival NDRs and so on and then it enters a loop on MakeUserSnippet to create all rows necessary for each participant. It ends with a series of loops writing the different sections of participant code onto the actual map.Function MakeMap(RTMap, ByRef Recips)

    Dim TimeHandlers, RejectHandlers, MainBodies    
    'COLLECTIONS TO HOLD ALL THE USERS ROW CATEGORIES
    Dim UserArray, k, j, TempAr
    
    'INSTANCIATE THE OBJECT
    Set TimeHandlers = CreateObject("Scripting.Dictionary")
    Set RejectHandlers = CreateObject("Scripting.dictionary")
    Set MainBodies = CreateObject("scripting.dictionary")
    
    'INSERT START UP ROWS, CHECK FOR RECEIPITS OR NON RELEVANT MESSAGES
    RTMap.InsertActivity -1, MakeRow(1, "ORSplit", 0, Array("IsNDR"), 1)
    RTMap.InsertActivity -1, MakeRow(2, "Goto", 0, Array(60110), 1)
    RTMap.InsertActivity -1, MakeRow(3, "OrSplit", 0, Array("IsReceipt"), 1)
    RTMap.InsertActivity -1, MakeRow(4, "Goto", 0, Array(60110), 1)
    RTMap.InsertActivity -1, MakeRow(9, "PreProcessing", 2, Array(False), 1)
    
    'CREATE USER ROWS FOR EACH RECIPIENT AND ADDS THEM TO THE COLLECTIONS
    For k = 1 To Recips.Count
        UserArray = MakeUserSnippet(Recips, k)
        MainBodies.Add CStr(k), UserArray(0)
        TimeHandlers.Add CStr(k), UserArray(1)
        RejectHandlers.Add CStr(k), UserArray(2)
    Next
    
    'INSERT THE MAIN BODIES OF EVERY USER'S ROWS IN THE MAP
    For k = 1 To MainBodies.Count
        TempAr = MainBodies.Item(CStr(k))
        For j = 0 To UBound(TempAr)
            RTMap.InsertActivity -1, TempAr(j)
        Next
    Next
  
    'INSERT THE TIMEOUT HANDLERS OF EVERY USER'S ROWS IN THE MAP
    For k = 1 To TimeHandlers.Count
        TempAr = TimeHandlers.Item(CStr(k))
        For j = 0 To UBound(TempAr)
            RTMap.InsertActivity -1, TempAr(j)
        Next
    Next
    
    'INSERT THE REJECT HANDLERS OF EVERY USER'S ROWS IN THE MAP
    For k = 1 To RejectHandlers.Count
        TempAr = RejectHandlers.Item(CStr(k))
        For j = 0 To UBound(TempAr)
            RTMap.InsertActivity -1, TempAr(j)
        Next
    Next

    'INSERT IF MESSAGE REJECTED CLEANUP CODE
    RTMap.InsertActivity -1, MakeRow(60000, "FinalizeReport", 2,
                                     Array(False,    False), 2)
    RTMap.InsertActivity -1, MakeRow(60005, "Send", 2, Array("<BLANK>", "", 
                                     False, "<FINALIZED>", "<ATTACH>", False, 
                                     False), 7)
    RTMap.InsertActivity -1, MakeRow(60010, "Terminate", 0, Null, 0)

    'INSERT IF MESSAGE APPROVED CLEANUP CODE
    RTMap.InsertActivity -1, MakeRow(60100, "FinalizeReport", 2, Array(True, 
                                     False), 2)
    RTMap.InsertActivity -1, MakeRow(60105, "Send", 2, Array("<BLANK>", "", 
                                     False, "<FINALIZED>", "<ATTACH>", False,  
                                     False), 7)
    RTMap.InsertActivity -1, MakeRow(60110, "Terminate", 0, Null, 0)
    
End Function

MakeUserSnippet creates three sections of Routing Map for each participant. The first and largest section is the list of activities that send a message and evaluate the response. The second and third sections handle timeouts and rejections respectively.

Function MakeUserSnippet(ByRef Recipients, k)
    Dim TimHan, RejHan, Body    
    'ARRAYS OF ROWS FOR EACH OF THE THREE PARTS OF A RECIPIENTS DATA
    
    'ROW VARS FOR THE MAIN BODY
    Dim SendRow, WaitRow, TimeOutSplitRow, GotoTimeHandlerA, IsNDRRow, 
        GotoTimeHandlerB, IsRecRow, GotoTimeHandlerC
    Dim ReceiveRow, ConsolidateRow, ApproveSplitRow, GotoNextRecipRow, 
        GotoRejectHandlerRow
    
    'ROW VARS FOR TIMEOUT HANDLING
    Dim TimeoutHandlerRow, GotoTerminRowA
    
    'ROW VARS FOR REJECT HANDLING
    Dim RejectHandlerRow, GotoTerminRowB
    
    
    'BEGIN CREATING THE MAIN BODY ROWS
    ID = k * 100
    Set SendRow = MakeRow(ID, "Send", 2, Array(Recipients.Item(k).Name, 
                          Recipients.Item(k).address, False, cdoMessage.Text, 
                          "<ATTACH>", False, False), 7)
    Set WaitRow = MakeRow(ID + 10, "Wait", 0, Array(24 * 60), 1)
    Set TimeOutSplitRow = MakeRow(ID + 15,"OrSplit",0,Array("IsTimeout"), 1)
    Set GotoTimeHandlerA = MakeRow(ID + 20, "Goto", 0, Array(k * 1000), 1)
    Set IsNDRRow = MakeRow(ID + 25, "OrSplit", 0, Array("IsNDR"), 1)
    Set GotoTimeHandlerB = MakeRow(ID + 30, "Goto", 0, Array(ID + 10), 1)
    Set IsRecRow = MakeRow(ID + 35, "OrSplit", 0, Array("IsReceipt"), 1)
    Set GotoTimeHandlerC = MakeRow(ID + 40, "Goto", 0, Array(ID + 10), 1)
    Set ReceiveRow = MakeRow(ID + 45, "Receive", 2, Array(False), 1)
    Set ConsolidateRow = MakeRow(ID + 50, "Consolidate", 2, Array(False), 1)
    Set ApproveSplitRow = MakeRow(ID + 55, "OrSplit", 0 , 
                                  Array("IsApprovalMsg"), 1)
    If k < Recipients.Count Then       
        Set GotoNextRecipRow = MakeRow(ID + 60, "Goto", 0, Array((k + 1) * 
                                       100), 1)
    Else
        Set GotoNextRecipRow = MakeRow(ID + 60, "Goto", 0, Array(60100), 1)
    End If
    Set GotoRejectHandlerRow = MakeRow(ID + 65,"Goto", 0,Array(k * 10000), 1)
    
    'BEGIN CREATING TIMEOUT HANDLER ROWS
    ID = k * 1000
    Set TimeoutHandlerRow = MakeRow(ID, "Goto", 0, Array(60000), 1)

    'BEGIN CREATING REJECT HANDLER ROWS
    ID = k * 10000
    Set RejectHandlerRow = MakeRow(ID, "Goto", 0, Array(60000), 1)
    
    'BUNDLE THE ROWS IN THEIR OWN ARRAYS
    Body = Array(SendRow, WaitRow, TimeOutSplitRow, GotoTimeHandlerA, 
                 IsNDRRow, GotoTimeHandlerB, IsRecRow, GotoTimeHandlerC, 
                 ReceiveRow, ConsolidateRow, ApproveSplitRow, 
                 GotoNextRecipRow, GotoRejectHandlerRow)
    TimHan = Array(TimeoutHandlerRow)
    RejHan = Array(RejectHandlerRow)

    'BUNDLE UP ALL THREE ARRAYS IN ONE AND RETURN IT
    MakeUserSnippet = Array(Body, TimHan, RejHan)
End Function

Figure 3 shows the map generated when the user selects just two users. The code to load the map into the form has been included purely for the purposes of demonstration.

Figure 3. The map generated by the sample form for two users

When the form is loaded in read mode, perhaps by the user choosing to check on the status of the route, the tracking table is visible. The tracking table is shown in Figure 4.

Figure 4. The tracking table for a process instance

The following code is executed to load the tracking table from the process instance message into the form.

Function OpenAsRead()
    Dim RTMap, RTRow, k, RowAr
    Dim RTRecipient, RTVote, RecipName, Argv
    
    SetObjects
    cmdRecip.Enabled = False

    Set RTMap = CreateObject("ExRt.Map")
    Set cdoMessage = cdoSession.GetMessage(Item.EntryID, 
                                           olkActEx.CurrentFolder.StoreID)

    RTMap.Message = cdoMessage
    RTMap.openmap 1
    
    'LIST ALL THE MAP ROWS IN THE LISTBOX ON THE "Map" PAGE
    If RTMap.ActivityCount > 0 Then
        ReDim RowAr(RTMap.ActivityCount, 3)
        For k = 1 To RTMap.ActivityCount
            Set RTRow = CreateObject("ExRt.Row.1")
            RTMap.GetRow k - 1, RTRow
            RowAr(k - 1, 0) = CStr(RTRow.ActivityID)
            RowAr(k - 1, 1) = RTRow.Action
            RowAr(k - 1, 2) = CStr(RTRow.Flags)
            RowAr(k - 1, 3) = ParamsToString(RTRow)
            
            If RTRow.Action = "Send" Then
                RTRow.GetArgs 1, Argv
                If Not (Argv(0) = "<BLANK>") 
                   Then txtRecip.Text = txtRecip.Text & Argv(0) & ";"
            End If
        Next
        lbMap.List() = RowAr
    End If
    
    
    'LIST STATUS OF ANSWERED RECIPIENTS IN THE "Recipients Table" PAGE
    Set RTVote = CreateObject("ExRt.VoteTable")
    RTVote.PIMessage = cdoMessage
    
    ReDim RowAr(RTVote.Count, 2)
    For k = 1 To RTVote.Count
        Set RTRecipient = RTVote.Item(k)
        RowAr(k - 1, 0) = RTRecipient.Recipient
        RowAr(k - 1, 1) = RTRecipient.status
        RowAr(k - 1, 2) = RTRecipient.Date
    Next
    lbRecipTrack.List() = RowAr
        
    lbMap.Enabled = True
End Function

To use this form requires two steps:

  1. The form itself must be installed. This is done in the same way as installing any other Outlook form.

  2. The event bindings must be established and the library of script functions saved with the bindings. Probably the easiest way to do this is to have the Exchange Server Administrator, or other suitably privileged individual, run the Routing Wizard that ships with the Exchange Server 5.5 Service Pack 1 CD to create a very simple route on your folder. A detailed description of how the binding process works is beyond the scope of this article, but the Exchange Server documentation gives a good description, and the source code for the Routing Wizard is available for those who want to automate this step.

Participants in the route can use any e-mail client that supports "mailto" URLs. This covers most of the e-mail clients available on the market today. Participating in a route usually involves simply selecting the appropriate mailto URL for approval or rejection. If the participant is using the Outlook messaging and collaboration client as the client, then the Outlook voting buttons can be used instead. The participant will also be able to go to the routing folder and examine the status of all the process instances using a view of the folder. Or the participant can drill down to see the status of all the individual participants in one particular process by looking at its Outlook tracking table.

Wider Application

There is a temptation to call the Exchange Server routing solution a workflow system. It is not. Rather it is just one component of a larger set that would have to be assembled to build such a system. For example, a complete system requires a serious design tool, sophisticated auditing, and, usually, interaction with other databases.

The samples and Routing Wizard provided do not exercise many of the capabilities of the Routing Engine. For example, one capability not exercised is the AndSplit function. This is implemented by the engine spawning a set of child processes, each as capable as the parent. Between this and the OrSplit, it is possible to create arbitrarily complex routes. The use of script for evaluation and functional activities allows scope for interaction with other workflow systems—for example, to handle exception processing while keeping the master workflow process up to date with tracking messages, and, as far as Exchange allows, transaction processing systems, such as Microsoft Transaction Server.

There are also several other features of Exchange Server routing that are not demonstrated in this sample: