When a .NET developer hears the words “You need to add workflow to the project,” the first thing that comes to mind is the idea of taking on Windows Workflow Foundation.
In 2010, we chose WF as a document flow engine.
The arguments are simple:
- Is free;
- Built into Visual Studio;
- There is a lot of information about using WF on the Internet.
For a year and a half (from August 2010 to February 2012) of using WF, we faced a lot of various problems in meeting customer requirements. In the end, we were forced to abandon Windows Workflow Foundation and make our implementation of State Machine.
')
In this article I will talk about the main problems that we faced, and how to solve (or not solved).
Introduction
In my opinion, there are two articles that describe well the use of WF in the Document Approval System.
For WWF 3:
“Document approval workflow system” ;
For WWF 4:
"Overview of Windows Workflow Foundation on the example of building an electronic document management system .
"They describe well, but describe only the tip of the iceberg.
In short, these articles describe how:
- Draw a diagram;
- How to move the document on the route;
- How to specify traffic conditions.
The implementation of even these simple operations requires very substantial labor costs and is not complete without crutches and dances with a tambourine. We danced with this tambourine.
Unlike a colleague from Luxoft, I
plucked up courage and laid out our implementation of the workflow module on WWF 3.5 "as is" in public access.
URL:
Budget.ServerBrief information about the projectThe project consists of two parts: client WinForms-application and server part.
The link published source of the server.
The server part is responsible for document circulation and integration with external systems.
Workflow schemes are in the Budget2.Workflow project (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 Workflow Foundation
You connected WF to the project, learned how to move the document along the route, indicated the conditions for changing status. How to do this is written in the articles that I cited above.
Then the fun begins ...

Getting a list of available commands for a user
WF does not support Commands and Actors (the author of the document, the head of the author, the controller, the manager).
It needs to be implemented independently. Moreover, if in the WWF 4 version you can get a list of Bookmarks, then in version 3.5 this could not be done and you had to store the list of commands for each state separately.

I will quote the author from the above article:
In addition, a separate set of metadata is separately stored for each General activity: launch privileges, document types for which the activity is allowed to run, Dynamic LINQ expression to the document for testing the launch capability, and others.
For
each activity
separately, you need to specify a set of metadata, by which then you need to check access.
That's right, we did the same.
Once this can be done, keeping it up to date is difficult.
Getting the list of incoming documents
We implement this requirement after the scheme has been implemented in WF.
The problem was simple enough: we could determine
if the user could agree on a specific document at the current stage , but
we could not get a list of all the users who could agree on a document at this stage . In the system of the order of 300-400 users, the problem was not solved by sorting.
This forced us to write a filter that made a selection of documents available for approval by the current user, depending on the user's roles, his place in the hierarchy of departments, document attributes and other parameters.

Filter exampleProcess statuses are listed in enum BillDemandStateEnum.
private string GetFilter() { List<Guid> 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<Guid> 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.
Schema versioning
There is no built-in versioning and updating process schema in Windows Workflow Foundation 3.5.
The situation hasn't changed in WF 4 -
Version handling in Workflow Foundation 4 .

If the process is running, then updating the march pattern is simply not possible. To update the scheme you need to have an old scheme and dance a little with a tambourine. We danced for about a week or two, but made a more or less working
mechanism for updating the schemes . Now our project is regularly updated with DDL with the names Workflow.xxx.dll, where xxx is the number of the old version.
Reconciliation history ... with enumeration of future stages
The implementation of the matching history is a trivial thing. It is necessary to save information in the nameplate who, when and on which button is pressed. But the simple story of client reconciliation did not suit.
The client wanted the system to show the list of remaining stages for approval (future stages) and for each such stage list the list of users who can approve the document separated by commas.

On this dance tambourine around the WF we are bored. Began to think how would we part with ... WF.
By the way, now we are solving this problem once or twice: there is a special mode in our product - Pre-Execution mode. Which allows you to make a single run along the route and form future stages and potential coordinators.
“Give us a designer”
To give the client a designer from WF, for obvious reasons, we could not. I do not remember how, but somehow convinced the client that you should not do this at this stage.

Dynamically add states to a schema
A year later, the client wanted the new states from a special directory to be added to the document’s route according to certain conditions.

We could not find a single example where the mechanism for generating the 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 migrate from WF to our development. The customer is treated with understanding. Thank you very much for it.
If someone implemented a similar case on WF, share an example, it is very interesting to look at it.Support
After successful implementation, the development of the system has not stopped. New requirements were received regularly.
We made changes to the route map and after each update we received bugs from the series:
- Why doesn't the user see the document that needs to be agreed?
- Why does the user see the document but cannot agree?
- Why does the user agree on the document, and his error takes off?
This is a typical situation for cases where the logic is duplicated (part of the conditions in WF, part in the metadata, part in the SQL filter for incoming).
Added to this is the fact that the WF gave out incomprehensible errors that clearly could not be interpreted. Several times I had to go to the site to the client.
Let's sum up
If you make an information system where there is a coordination function, then with a 99% probability 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.
For ourselves, we made a choice - we wrote our Workflow Engine .NET engine and successfully apply it in our projects.
In it, we took into account our experience in implementing a class of systems - the Document Approval System.
