Workflow in Document Approval System

When a .NET developer hears the words “You need to add workflow to the project,” the first idea comes to take the Windows Workflow Foundation.

In 2010, we selected WF as the workflow engine.

The arguments are simple:
  • Is free;
  • Built in to Visual Studio;
  • There is a lot of information on the Internet about using WF.

For a year and a half (from August 2010 to February 2012) of using WF, we encountered a lot of various problems in implementing customer requirements. Ultimately, we were forced to abandon the Windows Workflow Foundation and make our implementation of the State Machine.

In this article I will talk about the main problems that we encountered, and how they solved (or did not solve).

Introduction


In my opinion, there are two articles that describe the use of WF in the Document Approval System pretty well.
For WWF 3: “Document approval workflow system” ;
For WWF 4: “An Overview of the Windows Workflow Foundation with the Example of Building an Electronic Document Management System” .

They describe it well, but describe only the tip of the iceberg.

In short, these articles describe how to:
  • Draw a diagram;
  • How to move a document along a route;
  • How to indicate traffic conditions.

The implementation of even these simple operations requires very substantial labor costs and can not do without crutches and dances with a tambourine. We danced with this tambourine too.

Unlike my colleague from Luxoft, I took courage and put our implementation of the workflow module on WWF 3.5 “as is” in public access.

URL: Budget.Server
Brief information about the project
The project consists of two parts: the client WinForms application and the server part.
The link contains the source of the server side.
The server part is responsible for document management and integration with external systems. Workflow

schemes are in the project Budget2.Workflow (We used WWF 3.5, but the same problems remained in WWF 4).
API for working with workflow in the file: Budget.Server \ API \ Services \ WorkflowAPI.cs

So let's go.

How We Fought the Workflow Foundation


You connected WF to the project, learned how to move the document along the route, indicated the conditions for changing the status. How to do this is written in the articles that I cited above.

Then the fun begins ...



Retrieving a List of Available Commands for a User


WF does not support Commands and Actors (document author, author’s chief, controller, manager).
This must be implemented independently. Moreover, if you can get the Bookmarks list in WWF 4, then this was not possible in version 3.5 and you had to store the list of commands for each state separately.



I will quote the author from the above article:
In addition, for each General activity, a certain set of metadata is stored separately: privileges to run, types of documents by which it is allowed to run activity, Dynamic LINQ - expression to the document to check whether it can be launched, and others.

For each activity, you must separately specify a set of metadata, according to which then you need to check access.
That's right, we did exactly the same.
Once this can be done, it is difficult to keep it up to date.

Getting a list of incoming documents


We will implement this requirement after the circuit has been implemented in WF.
The problem was simple before the banality: we could determine whether the user at the current stage can coordinate a specific document , but we could not get a list of all users who can coordinate a document at this stage . In the system of about 300-400 users, it was not possible to solve the problem by exhaustive search.

This forced us to write a filter that made a selection of documents available for approval by the current user depending on the roles of the user, his place in the hierarchy of units, document attributes and other parameters.



Filter example
Process statuses are listed in the enum BillDemandStateEnum.

private string GetFilter()
{
    List deputyIds =
        DeputyEmployeeRepository.GetReplaceableEmployeeIdentityIds(DocumentType.BillDemand,EmployeeRepository.CurrentEmployee,
                                                                    true);
    string idsString = StringUtil.GetString(deputyIds);
    string opSubfilter =
        string.Format(
            "SELECT dbd.{0} FROM {1} dbd INNER JOIN {2} ON dbd.{0} = {3} INNER JOIN {4} ON {5} = {6} LEFT OUTER JOIN {7} ON dbd.{0} = {8} AND {9} = {10} WHERE {11} IS NULL",
            BillDemandTableBase.SelectColumn_Id,
            BillDemandTableBase.DEFAULT_NAME,
            BillDemandDistributionTableBase.DEFAULT_NAME,
            BillDemandDistributionTableBase.FilterColumn_BillDemandDistribution_BillDemandId,
            DemandTableBase.DEFAULT_NAME,
            BillDemandDistributionTableBase.FilterColumn_BillDemandDistribution_DemandId,
            DemandTableBase.FilterColumn_Demand_Id,
            WorkflowSightingTableBase.DEFAULT_NAME,
            WorkflowSightingTableBase.FilterColumn_WorkflowSighting_EntityId,
            DemandTableBase.FilterColumn_Demand_ExecutorStructId,
            WorkflowSightingTableBase.FilterColumn_WorkflowSighting_ItemId,
            WorkflowSightingTableBase.FilterColumn_WorkflowSighting_Id
            );
    string limitSubfilter =
        string.Format(
            "SELECT {0} FROM {1} WHERE {2} = {3} AND {4} = {5} AND {6} IS NULL AND {7} IN ({8}) ",
            WorkflowSightingTableBase.SelectColumn_Id, WorkflowSightingTableBase.DEFAULT_NAME,
            BillDemandTableBase.FilterColumn_BillDemand_Id,
            WorkflowSightingTableBase.FilterColumn_WorkflowSighting_EntityId,
            WorkflowSightingTableBase.SelectColumn_StateId,
            BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
            WorkflowSightingTableBase.SelectColumn_SightingTime,
            WorkflowSightingTableBase.SelectColumn_SighterId, idsString);
    string filter =
        string.Format(
            "({0} IN ({1},{2}) AND {3} IN ({4})) OR ( EXISTS ({5}) )",
            BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
            (int) BillDemandStateEnum.Draft,
            (int) BillDemandStateEnum.PostingAccounting,
            BillDemandTableBase.FilterColumn_BillDemand_AuthorId,
            idsString,
            limitSubfilter);
    if (SecurityHelper.IsPrincipalsInRole(deputyIds, SecurityHelper.ControllerRoleId))
        filter += string.Format(" OR {0} = {1}", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                                (int) BillDemandStateEnum.UPKZControllerSighting);
    if (SecurityHelper.IsPrincipalsInRole(deputyIds, SecurityHelper.CuratorRoleId))
        filter += string.Format(" OR {0} = {1}", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                                (int) BillDemandStateEnum.UPKZCuratorSighting);
    if (SecurityHelper.IsPrincipalsInRole(deputyIds, SecurityHelper.UPKZHeadRoleId))
        filter += string.Format(" OR {0} = {1}", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                                (int) BillDemandStateEnum.UPKZHeadSighting);
    if (SecurityHelper.IsPrincipalsAccountant(deputyIds, BudgetPart))
    {
        if (CommonSettings.CheckAccountingInFilial)
        {
            filter += string.Format(" OR ({0} = {1} AND {2} = '{3}')",
                                    BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                                    (int) BillDemandStateEnum.InAccounting,
                                    BillDemandTableBase.FilterColumn_BillDemand_FilialId,
                                    EmployeeRepository.CurrentEmployeeFilialId);
        }
        else
        {
            filter += string.Format(" OR ({0} = {1})",
                                    BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                                    (int)BillDemandStateEnum.InAccounting
                                    );
        }
    }
    List deputyDivisionHeads = SecurityHelper.GetPrincipalsDivisionHead(deputyIds, BudgetPart);
    if (deputyDivisionHeads.Count > 0)
    {
        string currentEmployeeChildrenStructs = EmployeeRepository.CurrentEmployeeChildrenStructs.Replace("(", "").Replace(")", "");
        string deputyDevisionHeadString = StringUtil.GetString(deputyDivisionHeads);
        filter +=
            string.Format(
                " OR ({0} = {1} AND EXISTS (SELECT vp.Id FROM [dbo].[vStructDivisionParentsAndThis] vp INNER JOIN {2} e ON vp.ParentId = e.{3} WHERE vp.Id = {4} AND e.{5} IN ({6})))",
                BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                (int)BillDemandStateEnum.HeadInitiatorSighting,
                EmployeeRepository.DEFAULT_NAME,
                EmployeeRepository.SelectColumn_StructDivisionId,
                BillDemandTableBase.FilterColumn_AuthorStructDivision_Id,
                EmployeeRepository.SelectColumn_SecurityTrusteeId,
                deputyDevisionHeadString
                );
        filter +=
            string.Format(
                " OR ({0} = {1} AND {2} > 0 AND EXISTS ({3} AND {4} IN ({5}) AND dbd.{6} = {7}))",
                BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId,
                (int) BillDemandStateEnum.LimitManagerSighting,
                BillDemandTableBase.FilterColumn_BillDemand_BudgetPartId, opSubfilter,
                DemandTableBase.FilterColumn_Demand_ExecutorStructId,
                currentEmployeeChildrenStructs,
                BillDemandTableBase.SelectColumn_Id,
                BillDemandTableBase.FilterColumn_BillDemand_Id);
    }
    return filter;
}


These filters took us 2 weeks.

Versioning Schemas


There are no built-in versioning and updating process flowcharts in Windows Workflow Foundation 3.5.
In WF 4, the situation has not changed - Version handling in Workflow Foundation 4 .



If the process is running, then updating the march scheme just doesn’t work out that way. To update the scheme, you need to have an old scheme and dance a little with a tambourine. They danced for about a week or two, but made a more or less working mechanism for updating the circuits . Now our project is regularly updated with DDL with the names Workflow.xxx.dll, where xxx is the number of the old version.

History of alignment ... with listings of future stages


The implementation of the history of reconciliation is a trivial thing. It is necessary to save information in the tablet who, when and which button is pressed. But the simple story of customer approval did not suit.

The client wanted the system to show a list of remaining steps for approval (future steps) and for each such step, list the users who can coordinate the document with a comma.



At this tambourine dancing around WF we were fed up. They began to think how we would part with ... WF.

By the way, now we are solving this problem for one or two: our product has a special mode - Pre-Execution mode. Which allows you to make a blank run on the route and form future stages and potential coordinators.


"Give us a designer"


For obvious reasons, we could not give the client a designer from WF. I don’t remember how, but somehow they convinced the client that they should not do this at this stage.



Dynamically add states to a schema


After a year, the client wanted new conditions from a special directory to be added to the document route under certain conditions.



We could not find a single example where a mechanism for generating a process diagram would be shown. Therefore, they did not even try to do it. We asked the client to wait a couple of months while we will migrate from WF to our development. Customer taking with understanding. Thank you very much for that.

If someone implemented a similar case on WF, share an example, it is very interesting to look at it.

Support


After a successful implementation, the development of the system did not stop. New requirements have been received regularly.
We made changes to the route scheme and after each update we received bugs from the series:

  • Why doesn’t the user see the document that is to be agreed?
  • Why does the user see the document but cannot reconcile?
  • Why does the user coordinate the document, and his error crashes?

This is a typical situation for cases where the logic is duplicated (part of the conditions in WF, part in metadata, part in the SQL filter for incoming).

To this was added the fact that WF produced incomprehensible errors that could not be clearly interpreted. Several times I had to go to the site to the client.

To summarize


If you are creating an information system where there is a coordination functionality, then with a probability of 99% you will encounter most of the cases listed above. Not every company can afford to implement this on WF. Not every customer will be willing to pay for it.

We made a choice for ourselves - we wrote our own Workflow Engine .NET engine and successfully use it in our projects.
In it, we took into account our experience in implementing class systems - Document Approval System.


Also popular now: