Introduction
Windows WorkFlow (WF) is a powerful framework for creating enterprise level workflows. Such power also brings great complexity. Much of WF is "roll your own", you are provided the core components but much of the required elements are up to you to provide. There are many ways of hosting workflows, this article describes hosting workflows using .asmx web services.
This example project shows a complete WF solution composed of a DotNetNuke module communicating with a WF workflow hosted in the Open Source project IWebWF (IWebWF.com). The project is called Vacation Requests and it allows users to make requests for time off from work. The requests will be processed by the workflow using the following business rules:
- If Employee has enough days available and they are requesting less than 4 days, automatically approve the request.
- If they don’t have enough days available, or more than 3 days are requested, then require approval.
This article examines the project using the following outline:
- Overview of Vacation Request 2.0
- Set-up
- Make a request
- How to call a web service dynamically
- Approve a request
- Create a CheckDigit and call the Vacation Request Workflow web service
- Call the DNN web service and update the record
- Logging
- DNN Module
- IWebWF
- Using the IWebCore for Logging and Emails
- Status page shows actively persisted workflows
- Details page identifies the workflow
- Security
- Workflow is "disconnected". It only uses web services in and out.
- The CheckDigit is used to open and close "the door"
Overview of Vacation Request 2.0
The following diagram shows the basic structure of the Vacation Request application:
The "front-end" of the application resides in a module running in the DotNetNuke website. The "back-end" processing for the application resides in the workflow running in the IWebWF website. The DotNetNuke module and the workflow communicate using web services.
The DotNetNuke module exposes the following web services:
- UpdateVacationRequest - Allows the workflow the ability to update a vacation request record.
- GetUserDetails - Provides details about a vacation request including the days a user has available.
The workflow exposes these web services:
- StartWorkflow - Allows the DNN module the ability to start a vacation request workflow.
- ApproveRequest - Allows the DNN module the ability to approve a vacation request.
WF workflows can be hosted in a number of different ways. A workflow could even be hosted in the DotNetNuke website. This design was chosen because it allows the solution to be scaled by adding multiple instances of the IWebWF application. In addition, the workflow components and persistence services can be resource intensive. It is desirable to off-load this from the DotNetNuke website.
Windows Communication Foundation (WCF) can be used instead of the .asmx web services described in this solution. While this works, WCF introduces complications when constructing the WCF services that provide unnecessary challenges, such as needing complex configuration files to bind the protocol and the transport.
Set-up
To set-up the application, you need to install IWebWF and DotNetNuke. You can download IWebWF from: http://www.codeplex.com/IWebWF and DotNetNuke from: http://DotNetNuke.com.
IWebWF
After installing IWebWF, log in as the administrator and configure and test the Email settings.
Unzip the files from the VacationRequestWorkflow.zip file and place the VacationRequest.dll file in the "Bin" directory and the VacationRequest.asmx file in the "Webservice" directory.
DotNetNuke
Install the
Vacation Request_02.00.00_Install.zip module using the
normal DotNetNuke module installation process (if using DNN4 Run
LinqPrep first).
Log in as an administrator and click the
[Set Webservice URL] link and set the web service link to point to the
VacationRequest.asmx page in the IWebWF site.
The
[Edit Users Vacation Days] (from the main page of the module) allows the administrator to set the available days for users.
Also, ensure the DNN admin account has an email address and the SMTP settings are configured in the DNN site.
Make a Request
A vacation request starts in the DNN module. It makes a web service call to the workflow web service. The DNN module creates a random number and saves it in the database as a CheckDigit. The RequestID, the CheckDigit, the Portal administrator and users email addresses are passed to the workflow web service.
Creating a proxy in DNN to call an external web service
To allow a DNN module to call an external web service and change the address dynamically, a
VacationWebService project is created that contains a normal .asmx web proxy that connects to the workflow web service in the IWebWF site.
The project is compiled and the
VacationWebService.dll file that is created, is placed in the "Bin" directory of the DNN site. This allows you to instantiate the class with code such as this:
VacationRequestWorkflow_WebService wsVacationRequest = new VacationRequestWorkflow_WebService();
and alter the web service address dynamically with code like this:
wsVacationRequest.Url = GetWebServiceURL();
The following, shows the complete code to create a random
CheckDigit code and call the workflow web service:
VacationRequestDAL VacationRequestDAL = new VacationRequestDAL();
VacationRequest VacationRequest = new VacationRequest();
VacationRequest.Approved = false;
VacationRequest.CheckDigit = GetRandomNumber();
VacationRequest.Complete = false;
VacationRequest.CreatedDate = DateTime.Now;
VacationRequest.DateRequested = Convert.ToDateTime(txtRequestedDate.Text);
VacationRequest.DaysRequested = Convert.ToInt32(txtRequestedDays.Text);
VacationRequest.LastActiveDate = DateTime.Now;
VacationRequest.NeedsApproval = false;
VacationRequest.UserID = UserId;
VacationRequest.WorkflowInstanceId = "";
VacationRequestDAL.VacationRequests.InsertOnSubmit(VacationRequest);
VacationRequestDAL.SubmitChanges();
VacationRequestWorkflow_WebService wsVacationRequest = new VacationRequestWorkflow_WebService();
wsVacationRequest.CookieContainer = new System.Net.CookieContainer();
wsVacationRequest.Url = GetWebServiceURL();
Guid WorkflowInstanceID = wsVacationRequest.StartWorkflow(VacationRequest.RequestID,
VacationRequest.CheckDigit, GetLocalWebserviceURL(), PortalSettings.Email, UserInfo.Email);
UpdateWorkflowInstanceID(VacationRequest.RequestID, WorkflowInstanceID);
pnlVacationRequest.Visible = false;
lblError.Text = "Request submitted. You will receive an email with a response";
VacationRequest Workflow
The VacationRequest workflow has an interface that defines the web services that it exposes (StartWorkflow and ApproveRequest):
public interface IVacationRequest
{
Guid StartWorkflow(int parmRecordID, int parmCheckDigit, string parmWebService,
string parmAdministratorEmailstring, string parmEmployeeEmail);
bool ApproveRequest(int parmRecordID, int parmCheckDigit, bool parmApproval);
}
The VacationRequestWorkflow.cs file contains the workflow logic for the workflow.
The StartVacationRequest activity is bound to the StartWorkflow web method that starts the workflow. After the workflow is started, the wsGetUserDetails activity calls the GetUserDetails web service in the DNN site.
The workflow is able to set the web address dynamically by wiring-up a method to the Invoking event and setting the url of the .asmx web proxy that points to the web service in the DNN module.
wsVacationRequest.WebService objWebService = (wsVacationRequest.WebService)e.WebServiceProxy;
objWebService.Url = WebService;
The workflow calls this web service method in the DNN site which returns information about the vacation request:
public UserDetails GetUserDetails(int RecordID, int CheckDigit)
{
UserDetails UserDetails = new UserDetails();
VacationRequestDAL VacationRequestDAL = new VacationRequestDAL();
var VacationRequestsResult = (from VacationRequests in VacationRequestDAL.VacationRequests
where VacationRequests.RequestID == RecordID &
VacationRequests.CheckDigit == CheckDigit &
VacationRequests.CheckDigit != 0
select VacationRequests).FirstOrDefault();
if (VacationRequestsResult != null)
{
var VacationDaysResult = (from Days in VacationRequestDAL.VacationDays
where Days.UserID == VacationRequestsResult.UserID
select Days).FirstOrDefault();
UserDetails.RequestedDate = VacationRequestsResult.DateRequested;
UserDetails.DaysRequested = VacationRequestsResult.DaysRequested;
UserDetails.VacationDays = VacationDaysResult.VacationDays;
}
else
{
UserDetails.RequestedDate = new DateTime(1900,1,1);
UserDetails.DaysRequested = 0;
UserDetails.VacationDays = 0;
}
return UserDetails;
}
After the workflow receives the information about the vacation request, the process reaches the ifElseActivity_ProcessRequest decision point. There are three possible activity groups that could be processed based on the values received from the DNN web service.
When you right-click on the TerminateWorkflowBranch...
and select Properties, you will be able to see the see the rule conditions that determine the execution of that branch (the days requested, and the vacation days are 0).
The same process can be performed to see the rule conditions for the NeedsApprovalBranch (the days requested is more than 3 days, or the days requested is more than the vacation days, or the vacation days minus the days requested would be less than 0).
(see this article for more information on using the rules engine)
The ApprovedBranch does not have a rule condition and will be executed by default if the rule conditions for TerminateWorkflowBranch and NeedsApprovalBranch are not met (the ifElseActivity branches are evaluated from left-to-right).
Approve a Request
If the days requested and the vacation days are 0 the workflow will terminate. If a request is for less than 4 days and there are sufficient days available, the request is automatically approved. An email is sent to the user, and the workflow terminates.
For all other situations, the NeedsApprovalBranch branch is executed. This branch contains activities that call the web service in the DNN site to update the record. It sets the record as needing approval, and sends an email to the administrator indicating that there is a record that needs approval.
The workflow then proceeds to the WaitForApproval activity. This activity contains the ApprovalWebService group.
The WaitForApproval activity is a WhileActivity that will stay in a loop until the Code Condition is met. It will not break out of the loop until the value of CheckIsTimeToStop method returns true.
The wsApprovalRequest_Input activity is the first activity in the ApprovalWebService group. The wsApprovalRequest_Input activity is bound to the ApproveRequest web method in the workflow. It will now wait for a web service call from the DNN site to approve the request.
DNN Module
When logged into the DNN site as an administrator, the [Approve Requests] link will take you to the approval page.
The following code creates a new random CheckDigit and calls the workflow web service:
bool boolApproval = false;
string strCommandArgument = (string)e.CommandArgument;
int intRecordID = Convert.ToInt32(e.CommandName);
int CheckDigit = GetRandomNumber();
Guid WorkflowInstanceID = UpdateCheckDigitAndGetWorkflowInstanceID(intRecordID, CheckDigit);
if (strCommandArgument == "Approve")
{
boolApproval = true;
}
else
{
boolApproval = false;
}
VacationRequestWorkflow_WebService wsVacationRequest = new VacationRequestWorkflow_WebService();
wsVacationRequest.CookieContainer = new System.Net.CookieContainer();
wsVacationRequest.Url = GetWebServiceURL();
Uri VacationRequestUri = new Uri(wsVacationRequest.Url);
wsVacationRequest.CookieContainer = new System.Net.CookieContainer();
CookieCollection mycollection = wsVacationRequest.CookieContainer.GetCookies(VacationRequestUri);
wsVacationRequest.CookieContainer.SetCookies
(
VacationRequestUri,
String.Format("{0}={1}", "WF_WorkflowInstanceId", WorkflowInstanceID.ToString())
);
wsVacationRequest.ApproveRequest(intRecordID, CheckDigit, boolApproval);
Vacation Request Workflow
The workflow receives the approval call from the DNN site and processes the activities in the ApprovalProcess activity group. The wsUpdateVacationRequestApproval activity calls the following UpdateVacationRequest web service method in the DNN site to update the record:
public bool UpdateVacationRequest(int RecordID, int CheckDigit, int VacationDays, bool NeedsApproval, bool Approved, bool Complete)
{
VacationRequestDAL VacationRequestDAL = new VacationRequestDAL();
var VacationRequestsResult = (from VacationRequests in VacationRequestDAL.VacationRequests
where VacationRequests.RequestID == RecordID &
VacationRequests.CheckDigit == CheckDigit &
VacationRequests.CheckDigit != 0
select VacationRequests).FirstOrDefault();
if (VacationRequestsResult != null)
{
VacationRequestsResult.Approved = Approved;
VacationRequestsResult.Complete = Complete;
VacationRequestsResult.NeedsApproval = NeedsApproval;
VacationRequestsResult.LastActiveDate = DateTime.Now;
VacationRequestsResult.CheckDigit = 0;
var VacationDaysResult = (from Days in VacationRequestDAL.VacationDays
where Days.UserID == VacationRequestsResult.UserID
select Days).FirstOrDefault();
VacationDaysResult.VacationDays = VacationDays;
VacationRequestDAL.SubmitChanges();
}
return true;
}
In the workflow, the isTimeToStop value is set to true which causes the CheckIsTimeToStop method to return true.
#region CheckIsTimeToStop
private void CheckIsTimeToStop(object sender, ConditionalEventArgs e)
{
e.Result = !(isTimeToStop);
}
#endregion
This causes
WaitForApproval activity to stop and the workflow terminates. The process is complete.
Logging
When workflows are running it is hard to know what is going on without logging.
The DNN module provides some logging by clicking on the [History] link (when logged in as an administrator).
The VacationRequest workflow project contains a reference to the IWebCore project that allows it to easily log actions with code such as this:
#region WriteToIWebWFLog
private void WriteToIWebWFLog(string LogEvent)
{
ApplicationLog.AddToLog(string.Format("VacationRequest Event - WorkflowInstanceID: {0} Event: {1}",
this.WorkflowInstanceId.ToString(), LogEvent));
}
private void WriteErrorToIWebWFLog(string Error)
{
ApplicationLog.AddToLog(string.Format("VacationRequest Error - WorkflowInstanceID: {0} Error: {1}",
this.WorkflowInstanceId.ToString(), Error));
}
#endregion
The IWebCore project also allows emails to be sent using code such as this:
#region SendEmailtoEmployee
private void SendEmailtoEmployee()
{
try
{
Email Email = new Email();
string strEmailMessage = String.Format("Your Vacation Request for {0} {1}.",
objUserDetails.RequestedDate.ToShortDateString(), (Approval) ? "has been approved" : "has been declined");
Email.SendMail(EmployeeEmail, "", "", "", "Email From VacationRequest",
strEmailMessage, "");
WriteToIWebWFLog(strEmailMessage);
}
catch (Exception ex)
{
WriteErrorToIWebWFLog(String.Format("{0} - {1} ", ex.Message, ex.StackTrace));
}
}
#endregion
The logged events show in the administration page in the IWebWF website.
In addition, the workflow status page in the IWebWF website shows any currently persisted workflows. These are workflows that are waiting for some sort of action before they can terminate. The persistence service automatically saves the state of idled workflows and automatically reactivates them when needed. This allows long running workflows to survive during server restarts.
Clicking on a workflow GUID on the Status page displays the workflow name and it's activities.
Security
Windows workflow allows you to design security in any way you prefer. This example uses the following security design:
- The workflow is "disconnected". It only communicates using web services
- The workflow does not "believe" any information passed to it. The available days for an employee cannot be passed to it directly. When the workflow is started, it uses the vacation RequestID and the CheckDigit to retrieve the information needed for it's calculations. This prevents a hacker from passing false information.
- The web service methods in the DNN module only allow records to be retrieved and updated for a short period of time. In most cases only a few seconds. The DNN module essentially "opens the door" by creating a record and sending the RequestID and the CheckDigit to the workflow. The workflow updates the record and then sets the CheckDigit to 0. This "closes the door" to the record.
Versioning
If you deploy a workflow and you change it while you still have workflows that are still running using the old version, you will get an error when those workflows try to re-load. To avoid this, you will need to use workflow versioning.
See this article for information on workflow versioning with IWebWF
Summary
The question remains, why would you want to go through all this trouble? This example barely qualifies as a justifiable use of WF. The reasons to use workflow rather than simple procedural code is:
- If the rules become more complex the workflow is easily adjusted
- The Workflow can perform jobs better and faster if complex calculations are needed to determine when a request is approved
In addition, using IWebWF eliminates a lot of unnecessary work.