Workflow Engine with .NET
Hello, I'm Feri, and today I'm excited to share my hands-on experience with workflow engines. In the dynamic world of business operations, efficiency is paramount. Workflow engines, like the one we'll be exploring today, play a pivotal role in automating, monitoring, and optimizing the flow of tasks within a business process.
Imagine a conductor guiding an orchestra — that's the essence of a workflow engine. It ensures each element of a process is executed in a timely and organized manner, aiming to enhance efficiency, reduce errors, and provide a transparent overview of the entire workflow.
In this tutorial, our objective is to dive into the practical steps of creating a workflow engine using workflowengine.io. We'll cover everything from setting up a database with SQL Server to initializing the WorkflowRuntime, creating a visual designer with an ASP.NET Core MVC web application, and running a simple workflow using a console application.
Let's embark on this exploration of workflow engines and discover how they can revolutionize the way we handle business processes.
Setting Up the Database for Workflow Engine
To initiate our workflow engine, we'll commence by establishing a dedicated database. In this instance, we'll leverage SQL Server for this purpose. The initial step involves creating a new database, a task easily accomplished using SQL Server Management Studio (SMSS). Subsequently, execute the provided query to set up the necessary tables and structures within the newly created database.
SQL Querry
/*
Company: OptimaJet
Project: WorkflowEngine.NET Provider for MSSQL and Azure SQL
Version: 11.0
File: CreatePersistenceObjects.sql
*/
BEGIN TRANSACTION
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessScheme'
)
BEGIN
CREATE TABLE WorkflowProcessScheme (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessScheme PRIMARY KEY
,[Scheme] NTEXT NOT NULL
,[DefiningParameters] NTEXT NOT NULL
,[DefiningParametersHash] NVARCHAR(24) NOT NULL
,[SchemeCode] NVARCHAR(256) NOT NULL
,[IsObsolete] BIT DEFAULT 0 NOT NULL
,[RootSchemeCode] NVARCHAR(256)
,[RootSchemeId] UNIQUEIDENTIFIER
,[AllowedActivities] NVARCHAR(max)
,[StartingTransition] NVARCHAR(max)
)
CREATE INDEX IX_SchemeCode_Hash_IsObsolete ON WorkflowProcessScheme (
SchemeCode
,DefiningParametersHash
,IsObsolete
)
PRINT 'WorkflowProcessScheme CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessInstance'
)
BEGIN
CREATE TABLE WorkflowProcessInstance (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessInstance PRIMARY KEY
,[StateName] NVARCHAR(max)
,[ActivityName] NVARCHAR(max) NOT NULL
,[SchemeId] UNIQUEIDENTIFIER
,[PreviousState] NVARCHAR(max)
,[PreviousStateForDirect] NVARCHAR(max)
,[PreviousStateForReverse] NVARCHAR(max)
,[PreviousActivity] NVARCHAR(max)
,[PreviousActivityForDirect] NVARCHAR(max)
,[PreviousActivityForReverse] NVARCHAR(max)
,[ParentProcessId] UNIQUEIDENTIFIER
,[RootProcessId] UNIQUEIDENTIFIER NOT NULL
,[IsDeterminingParametersChanged] BIT DEFAULT 0 NOT NULL
,[TenantId] NVARCHAR(1024)
,[StartingTransition] NVARCHAR(max)
,[SubprocessName] NVARCHAR(max)
,[CreationDate] datetime NOT NULL CONSTRAINT DF_WorkflowProcessInstance_CreationDate DEFAULT getdate()
,[LastTransitionDate] datetime NULL
,[CalendarName] NVARCHAR(450)
)
CREATE INDEX IX_RootProcessId ON WorkflowProcessInstance (RootProcessId)
CREATE INDEX IX_CalendarName ON WorkflowProcessInstance (CalendarName)
PRINT 'WorkflowProcessInstance CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessInstancePersistence'
)
BEGIN
CREATE TABLE WorkflowProcessInstancePersistence (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessInstancePersistence PRIMARY KEY NONCLUSTERED
,[ProcessId] UNIQUEIDENTIFIER NOT NULL
,[ParameterName] NVARCHAR(max) NOT NULL
,[Value] NVARCHAR(max) NOT NULL
)
CREATE CLUSTERED INDEX IX_ProcessId_Clustered ON WorkflowProcessInstancePersistence (ProcessId)
PRINT 'WorkflowProcessInstancePersistence CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessTransitionHistory'
)
BEGIN
CREATE TABLE WorkflowProcessTransitionHistory (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessTransitionHistory PRIMARY KEY NONCLUSTERED
,[ProcessId] UNIQUEIDENTIFIER NOT NULL
,[ExecutorIdentityId] NVARCHAR(256)
,[ActorIdentityId] NVARCHAR(256)
,[ExecutorName] NVARCHAR(256)
,[ActorName] NVARCHAR(256)
,[FromActivityName] NVARCHAR(max) NOT NULL
,[ToActivityName] NVARCHAR(max) NOT NULL
,[ToStateName] NVARCHAR(max)
,[TransitionTime] DATETIME NOT NULL
,[TransitionClassifier] NVARCHAR(max) NOT NULL
,[IsFinalised] BIT NOT NULL
,[FromStateName] NVARCHAR(max)
,[TriggerName] NVARCHAR(max)
,[StartTransitionTime] DATETIME
,[TransitionDuration] BIGINT
)
CREATE CLUSTERED INDEX IX_ProcessId_Clustered ON WorkflowProcessTransitionHistory (ProcessId)
CREATE INDEX IX_ExecutorIdentityId ON WorkflowProcessTransitionHistory (ExecutorIdentityId)
PRINT 'WorkflowProcessTransitionHistory CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessInstanceStatus'
)
BEGIN
CREATE TABLE WorkflowProcessInstanceStatus (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessInstanceStatus PRIMARY KEY
,[Status] TINYINT NOT NULL
,[Lock] UNIQUEIDENTIFIER NOT NULL
,[RuntimeId] nvarchar(450) NOT NULL
,[SetTime] datetime NOT NULL
)
CREATE NONCLUSTERED INDEX [IX_WorkflowProcessInstanceStatus_Status] ON [dbo].[WorkflowProcessInstanceStatus]
(
[Status] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
CREATE NONCLUSTERED INDEX [IX_WorkflowProcessInstanceStatus_Status_Runtime] ON [dbo].[WorkflowProcessInstanceStatus]
(
[Status] ASC,
[RuntimeId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
PRINT 'WorkflowProcessInstanceStatus CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM sys.procedures
WHERE name = N'spWorkflowProcessResetRunningStatus'
)
BEGIN
EXECUTE (
'CREATE PROCEDURE [spWorkflowProcessResetRunningStatus]
AS
BEGIN
UPDATE [WorkflowProcessInstanceStatus] SET [WorkflowProcessInstanceStatus].[Status] = 2 WHERE [WorkflowProcessInstanceStatus].[Status] = 1
END'
)
PRINT 'spWorkflowProcessResetRunningStatus CREATE PROCEDURE'
END
IF NOT EXISTS (
SELECT 1
FROM sys.procedures
WHERE name = N'DropUnusedWorkflowProcessScheme'
)
BEGIN
EXECUTE (
'CREATE PROCEDURE [DropUnusedWorkflowProcessScheme]
AS
BEGIN
DELETE wps FROM WorkflowProcessScheme AS wps
WHERE wps.IsObsolete = 1
AND NOT EXISTS (SELECT * FROM WorkflowProcessInstance wpi WHERE wpi.SchemeId = wps.Id )
RETURN (SELECT COUNT(*)
FROM WorkflowProcessInstance wpi LEFT OUTER JOIN WorkflowProcessScheme wps ON wpi.SchemeId = wps.Id
WHERE wps.Id IS NULL)
END'
)
PRINT 'DropUnusedWorkflowProcessScheme CREATE PROCEDURE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowScheme'
)
BEGIN
CREATE TABLE WorkflowScheme (
[Code] NVARCHAR(256) NOT NULL CONSTRAINT PK_WorkflowScheme PRIMARY KEY,
[Scheme] NVARCHAR(max) NOT NULL,
[CanBeInlined] [bit] NOT NULL DEFAULT(0),
[InlinedSchemes] [nvarchar](max) NULL,
[Tags] [nvarchar](max) NULL,
)
PRINT 'WorkflowScheme CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowInbox'
)
BEGIN
CREATE TABLE WorkflowInbox (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowInbox PRIMARY KEY NONCLUSTERED
,[ProcessId] UNIQUEIDENTIFIER NOT NULL
,[IdentityId] NVARCHAR(256) NOT NULL
,[AddingDate] DATETIME NOT NULL DEFAULT GETDATE()
,[AvailableCommands] NVARCHAR(max) NOT NULL DEFAULT ''
)
CREATE CLUSTERED INDEX IX_IdentityId_Clustered ON WorkflowInbox (IdentityId)
CREATE INDEX IX_ProcessId ON WorkflowInbox (ProcessId)
PRINT 'WorkflowInbox CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM sys.procedures
WHERE name = N'DropWorkflowInbox'
)
BEGIN
EXECUTE (
'CREATE PROCEDURE [DropWorkflowInbox]
@processId uniqueidentifier
AS
BEGIN
BEGIN TRAN
DELETE FROM dbo.WorkflowInbox WHERE ProcessId = @processId
COMMIT TRAN
END'
)
PRINT 'DropWorkflowInbox CREATE PROCEDURE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessTimer'
)
BEGIN
CREATE TABLE WorkflowProcessTimer (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessTimer PRIMARY KEY NONCLUSTERED
,[ProcessId] UNIQUEIDENTIFIER NOT NULL
,[RootProcessId] UNIQUEIDENTIFIER NOT NULL
,[Name] NVARCHAR(max) NOT NULL
,[NextExecutionDateTime] DATETIME NOT NULL
,[Ignore] BIT NOT NULL
)
CREATE CLUSTERED INDEX IX_NextExecutionDateTime_Clustered ON WorkflowProcessTimer (NextExecutionDateTime)
CREATE INDEX IX_ProcessId ON WorkflowProcessTimer (ProcessId)
PRINT 'WorkflowProcessTimer CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowProcessAssignment'
)
BEGIN
CREATE TABLE WorkflowProcessAssignment (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowProcessAssignment PRIMARY KEY NONCLUSTERED
,[AssignmentCode] NVARCHAR(2048) NOT NULL
,[ProcessId] UNIQUEIDENTIFIER NOT NULL
,[Name] NVARCHAR(max) NOT NULL
,[Description] NVARCHAR(max)
,[StatusState] NVARCHAR(max) NOT NULL
,[DateCreation] DATETIME NOT NULL
,[DateStart] DATETIME
,[DateFinish] DATETIME
,[DeadlineToStart] DATETIME
,[DeadlineToComplete] DATETIME
,[Executor] NVARCHAR(256) NOT NULL
,[Observers] NVARCHAR(max)
,[Tags] NVARCHAR(max)
,[IsActive] BIT NOT NULL
,[IsDeleted] BIT NOT NULL
)
CREATE INDEX IX_Assignment_ProcessId ON WorkflowProcessAssignment (ProcessId)
CREATE INDEX IX_Assignment_AssignmentCode ON WorkflowProcessAssignment (AssignmentCode)
CREATE INDEX IX_Assignment_Executor ON WorkflowProcessAssignment (Executor)
CREATE INDEX IX_Assignment_ProcessId_Executor ON WorkflowProcessAssignment (ProcessId, Executor)
PRINT 'WorkflowProcessAssignment CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowGlobalParameter'
)
BEGIN
CREATE TABLE WorkflowGlobalParameter (
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowGlobalParameter PRIMARY KEY NONCLUSTERED
,[Type] NVARCHAR(306) NOT NULL
,[Name] NVARCHAR(128) NOT NULL
,[Value] NVARCHAR(max) NOT NULL
)
CREATE UNIQUE CLUSTERED INDEX IX_Type_Name_Clustered ON WorkflowGlobalParameter (
Type
,Name
)
PRINT 'WorkflowGlobalParameter CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowRuntime'
)
BEGIN
CREATE TABLE WorkflowRuntime (
[RuntimeId] nvarchar(450) NOT NULL CONSTRAINT PK_WorkflowRuntime PRIMARY KEY
,[Lock] UNIQUEIDENTIFIER NOT NULL
,[Status] TINYINT NOT NULL
,[RestorerId] nvarchar(450)
,[NextTimerTime] datetime
,[NextServiceTimerTime] datetime
,[LastAliveSignal] datetime
)
PRINT 'WorkflowRuntime CREATE TABLE'
EXEC('INSERT INTO WorkflowRuntime (RuntimeId,Lock,Status) VALUES (''00000000-0000-0000-0000-000000000000'', NEWID(),100)');
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowSync'
)
BEGIN
CREATE TABLE WorkflowSync (
[Name] nvarchar(450) NOT NULL CONSTRAINT PK_WorkflowSync PRIMARY KEY
,[Lock] UNIQUEIDENTIFIER NOT NULL
)
INSERT INTO [dbo].[WorkflowSync]
([Name]
,[Lock])
VALUES
('Timer',
NEWID());
INSERT INTO [dbo].[WorkflowSync]
([Name]
,[Lock])
VALUES
('ServiceTimer',
NEWID());
PRINT 'WorkflowSync CREATE TABLE'
END
IF NOT EXISTS (
SELECT 1
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [TABLE_NAME] = N'WorkflowApprovalHistory'
)
BEGIN
CREATE TABLE [dbo].[WorkflowApprovalHistory](
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_WorkflowApprovalHistory PRIMARY KEY NONCLUSTERED
,[ProcessId] UNIQUEIDENTIFIER NOT NULL
,[IdentityId] NVARCHAR(256) NULL
,[AllowedTo] NVARCHAR(max) NULL
,[TransitionTime] DateTime NULL
,[Sort] BIGINT NULL
,[InitialState] NVARCHAR(1024) NOT NULL
,[DestinationState] NVARCHAR(1024) NOT NULL
,[TriggerName] NVARCHAR(1024) NULL
,[Commentary] NVARCHAR(max) NULL
)
CREATE CLUSTERED INDEX IX_ProcessId_Clustered ON WorkflowApprovalHistory (ProcessId)
CREATE NONCLUSTERED INDEX IX_IdentityId ON WorkflowApprovalHistory (IdentityId)
PRINT 'WorkflowApprovalHistory CREATE TABLE'
END
COMMIT TRANSACTION
This process will generate essential tables within the database tailored for our workflow engine. Below is a snapshot showcasing the resulting database structure:
This schema lays the groundwork for a robust workflow engine, ensuring that the necessary components are in place for efficient task automation and management.
Initializing WorkflowRuntime
In this phase, we'll set up the foundational components of our workflow engine. Follow the steps below to initialize the WorkflowRuntime and establish the necessary infrastructure for seamless workflow management.
Create a Blank Solution:
Start by creating an empty solution; it should serve as the container for our workflow engine project.
Add a Class Library:
Within this solution, incorporate a new class library project. For illustration, let's name it "WorkflowLib."
Left click Solution > Add > New Project, then add new class library
Integrate the required NuGet packages to empower our project:
Once installed, the necessary assemblies will seamlessly augment our project.
Edit and Rename the Default Class:
Modify the default Class.cs file within our newly created class library.
Rename it to "WorkflowInit.cs" and implement the following code:
WorkflowInit.cs
using System;
using System.Xml.Linq;
using OptimaJet.Workflow.Core.Builder;
using OptimaJet.Workflow.Core.Bus;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.DbPersistence;
namespace WorkflowLib
{
public static class WorkflowInit
{
private static readonly Lazy<WorkflowRuntime> LazyRuntime = new Lazy<WorkflowRuntime>(InitWorkflowRuntime);
public static WorkflowRuntime Runtime
{
get { return LazyRuntime.Value; }
}
public static string ConnectionString { get; set; }
private static WorkflowRuntime InitWorkflowRuntime()
{
if (string.IsNullOrEmpty(ConnectionString))
{
throw new Exception("Please init ConnectionString before calling the Runtime!");
}
var dbProvider = new MSSQLProvider(ConnectionString);
var builder = new WorkflowBuilder<XElement>(
dbProvider,
new OptimaJet.Workflow.Core.Parser.XmlWorkflowParser(),
dbProvider
).WithDefaultCache();
var runtime = new WorkflowRuntime()
.WithBuilder(builder)
.WithPersistenceProvider(dbProvider)
.EnableCodeActions()
.SwitchAutoUpdateSchemeBeforeGetAvailableCommandsOn()
.AsSingleServer();
var plugin = new OptimaJet.Workflow.Plugins.BasicPlugin();
runtime.WithPlugin(plugin);
runtime.ProcessActivityChanged += (sender, args) => { };
runtime.ProcessStatusChanged += (sender, args) => { };
runtime.Start();
return runtime;
}
}
}
Create Designer
In this section, we'll set up a robust environment for designing workflows. Follow these steps to create an ASP.NET Core MVC web application, integrate the necessary NuGet packages, add controllers, and establish the designer's visual interface.
Create an ASP.NET Core MVC Application:
Left click Solution > Add > New Project, then add new ASP.NET Core MVC web App (Model-View-Controller)
Add Required Packages
Ensure the WorkflowLib is referenced by navigating to Add>ProjectReverence > check mark the WorkflowLib
领英推荐
Create DesignerController
Generate a new controller named DesignerController.cs to handle interactions with the designer interface. Include the necessary code to manage requests and responses.
using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OptimaJet.Workflow;
using WorkflowLib;
namespace WorkflowDesigner.Controllers
{
public class DesignerController : Controller
{
public IActionResult Index()
{
return View();
}
public async Task<IActionResult> Api()
{
Stream? filestream = null;
var parameters = new NameValueCollection();
var isPost = Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase);
foreach (var q in Request.Query)
{
parameters.Add(q.Key, q.Value.First());
}
if (isPost)
{
var keys = parameters.AllKeys;
foreach (var key in Request.Form.Keys)
{
if (!keys.Contains(key))
{
parameters.Add(key, Request.Form[key]);
}
}
if (Request.Form.Files.Count > 0)
{
filestream = Request.Form.Files[0].OpenReadStream();
}
}
var (result, hasError) = await WorkflowInit.Runtime.DesignerAPIAsync(parameters, filestream);
if (parameters["operation"]?.ToLower() == "downloadscheme" && !hasError)
return File(Encoding.UTF8.GetBytes(result), "text/xml");
if (parameters["operation"]?.ToLower() == "downloadschemebpmn" && !hasError)
return File(Encoding.UTF8.GetBytes(result), "text/xml");
return Content(result);
}
}
}
Establish a "Designer" folder within the "Views" directory and add an Index.cshtml file. This file will manage requests from the designer to the backend.
@model dynamic
@{
ViewBag.Title = "Designer";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<link href="~/css/workflowdesigner.min.css" rel="stylesheet" type="text/css" />
<script src="~/js/workflowdesigner.min.js" type="text/javascript"></script>
<script src="~/lib/jquery/dist/jquery.min.js" type="text/javascript"></script>
<form action="" id="uploadform" method="post" enctype="multipart/form-data" onsubmit="tmp()" style="padding-bottom: 8px;">
<input type="file" name="uploadFile" id="uploadFile" style="display:none" onchange="javascript: UploadScheme(this);">
</form>
<div id="wfdesigner" style="min-height:600px; max-width: 1200px;"></div>
<script>
var QueryString = function () {
var query_string = {};
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (typeof query_string[pair[0]] === "undefined") {
query_string[pair[0]] = pair[1];
} else if (typeof query_string[pair[0]] === "string") {
var arr = [query_string[pair[0]], pair[1]];
query_string[pair[0]] = arr;
} else {
query_string[pair[0]].push(pair[1]);
}
}
return query_string;
}();
var schemecode = QueryString.code ? QueryString.code : 'SimpleWF';
var processid = QueryString.processid;
var graphwidth = 1200;
var graphminheight = 600;
var graphheight = graphminheight;
var wfdesigner = undefined;
//Recreate designer object
function wfdesignerRedraw() {
var data;
if (wfdesigner != undefined) {
wfdesigner.destroy();
}
wfdesigner = new WorkflowDesigner({
name: 'simpledesigner',
apiurl: '/Designer/API',
renderTo: 'wfdesigner',
templatefolder: '/templates/',
graphwidth: graphwidth,
graphheight: graphheight
});
if (data == undefined) {
var isreadonly = false;
if (processid != undefined && processid != '')
isreadonly = true;
var p = { schemecode: schemecode, processid: processid, readonly: isreadonly };
if (wfdesigner.exists(p))
wfdesigner.load(p);
else
wfdesigner.create(schemecode);
}
else {
wfdesigner.data = data;
wfdesigner.render();
}
}
wfdesignerRedraw();
//Adjusts the size of the designer window
$(window).resize(function () {
if (window.wfResizeTimer) {
clearTimeout(window.wfResizeTimer);
window.wfResizeTimer = undefined;
}
window.wfResizeTimer = setTimeout(function () {
var w = $(window).width();
var h = $(window).height();
if (w > 300)
graphwidth = w - 40;
if (h > 300)
graphheight = h - 250;
if (graphheight < graphminheight)
graphheight = graphminheight;
wfdesigner.resize(graphwidth, graphheight);
}, 150);
});
$(window).resize();
$(window).resize();
//Add Control functions
function DownloadScheme() {
wfdesigner.downloadscheme();
}
function DownloadSchemeBPMN() {
wfdesigner.downloadschemeBPMN();
}
var selectSchemeType;
function SelectScheme(type) {
if (type)
selectSchemeType = type;
var file = $('#uploadFile');
file.trigger('click');
}
function UploadScheme(form) {
if (form.value == "")
return;
if (selectSchemeType == "bpmn") {
wfdesigner.uploadschemeBPMN($('#uploadform')[0], function () {
wfdesigner.autoarrangement();
alert('The file is uploaded!');
});
}
else {
wfdesigner.uploadscheme($('#uploadform')[0], function () {
alert('The file is uploaded!');
});
}
}
function OnSave() {
wfdesigner.schemecode = schemecode;
var err = wfdesigner.validate();
if (err != undefined && err.length > 0) {
alert(err);
}
else {
wfdesigner.save(function () {
alert('The scheme is saved!');
});
}
}
function OnNew() {
wfdesigner.create();
}
</script>
Edit the Index.cshtml file in the "Views/Home/Index.cshtml" folder to direct users to the newly created designer page.
@{
ViewData["Title"] = " Home Page";
}
<div class="text-center">
<h1 class="display-4">
Welcome
</h1>
<p>
<a href="/Designer/Index">
Open designer here
</a>
</p>
</div>
Now we need to download the files template official repositories then put it on wwwroot folder
Then we need to connect the database into the Designer add this line to the appsettings.json
"ConnectionStrings": {
"DefaultConnection": "Data Source=[Server Name];Initial Catalog=[Database Name];User ID=[User Name];Password=[Password Name]"
},
we also need to link the database connection to the Startup.cs
...
using WorkflowLib;
namespace WorkflowDesigner
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
WorkflowInit.ConnectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddControllersWithViews();
}
...
now we can try run the Designer
then we can open the designer page from the Home page
Explore the designer interface and understand how to create workflows.
we can check this video if want to understand how to make design using workflow engine, on my example it just a simple workflow design.
Creating the Application: Processes and Commands
Now, let's build the application that interacts with our workflow engine. Follow these steps to create a console application, add the necessary references, and execute commands on your workflow processes.
Begin by adding a new console application to your solution.
then we also need to add project reference to this
and also install dependency to this project
Add an appsettings.json file to your console application project. Include the database connection string:
"ConnectionStrings": {
"DefaultConnection": "Data Source=[Server Name];Initial Catalog=[Database Name];User ID=[User Name];Password=[Password Name]"
},
Update the Program.cs file with the necessary code for our console application. This code sets up the configuration, establishes the connection to the database, and provides options for various operations:
using System;
using OptimaJet.Workflow.Core.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using WorkflowLib;
using System.IO;
using Microsoft.Extensions.Configuration;
namespace ConsoleApp
{
class Program
{
static string schemeCode = "SimpleWF";
static Guid? processId = null;
static void Main(string[] args)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build();
WorkflowInit.ConnectionString = configuration[$"ConnectionStrings:DefaultConnection"];
// ------------------------------------------------------
Console.WriteLine("Operation:");
Console.WriteLine("0 - CreateInstance");
Console.WriteLine("1 - GetAvailableCommands");
Console.WriteLine("2 - ExecuteCommand");
Console.WriteLine("3 - GetAvailableState");
Console.WriteLine("4 - SetState");
Console.WriteLine("5 - DeleteProcess");
Console.WriteLine("9 - Exit");
Console.WriteLine("The process isn't created.");
CreateInstance();
do
{
if (processId.HasValue)
{
Console.WriteLine("ProcessId = '{0}'. CurrentState: {1}, CurrentActivity: {2}",
processId,
WorkflowInit.Runtime.GetCurrentStateName(processId.Value),
WorkflowInit.Runtime.GetCurrentActivityName(processId.Value));
}
Console.Write("Enter code of operation:");
char operation = Console.ReadLine().FirstOrDefault();
switch (operation)
{
case '0':
CreateInstance();
break;
case '1':
GetAvailableCommands();
break;
case '2':
ExecuteCommand();
break;
case '3':
GetAvailableState();
break;
case '4':
SetState();
break;
case '5':
DeleteProcess();
break;
case '9':
return;
default:
Console.WriteLine("Unknown code. Please, repeat.");
break;
}
Console.WriteLine();
} while (true);
}
private static void CreateInstance()
{
processId = Guid.NewGuid();
try
{
WorkflowInit.Runtime.CreateInstance(schemeCode, processId.Value);
Console.WriteLine("CreateInstance - OK.", processId);
}
catch (Exception ex)
{
Console.WriteLine("CreateInstance - Exception: {0}", ex.Message);
processId = null;
}
}
private static void GetAvailableCommands()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
var commands = WorkflowInit.Runtime.GetAvailableCommands(processId.Value, string.Empty);
Console.WriteLine("Available commands:");
if (commands.Count() == 0)
{
Console.WriteLine("Not found!");
}
else
{
foreach (var command in commands)
{
Console.WriteLine("- {0} (LocalizedName:{1}, Classifier:{2})",
command.CommandName, command.LocalizedName, command.Classifier);
}
}
}
private static void ExecuteCommand()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
WorkflowCommand command = null;
do
{
GetAvailableCommands();
Console.Write("Enter command:");
var commandName = Console.ReadLine().ToLower().Trim();
if (commandName == string.Empty)
return;
command = WorkflowInit.Runtime
.GetAvailableCommands(processId.Value, string.Empty)
.Where(c => c.CommandName.Trim().ToLower() == commandName).FirstOrDefault();
if (command == null)
Console.WriteLine("The command isn't found.");
} while (command == null);
WorkflowInit.Runtime.ExecuteCommand(command, string.Empty, string.Empty);
Console.WriteLine("ExecuteCommand - OK.", processId);
}
private static void GetAvailableState()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
var states = WorkflowInit.Runtime
.GetAvailableStateToSet(processId.Value, Thread.CurrentThread.CurrentCulture);
Console.WriteLine("Available state to set:");
if (states.Count() == 0)
{
Console.WriteLine("Not found!");
}
else
{
foreach (var state in states)
{
Console.WriteLine("- {0}", state.Name);
}
}
}
private static void SetState()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
string stateName = string.Empty;
WorkflowState state;
do
{
GetAvailableState();
Console.Write("Enter state:");
stateName = Console.ReadLine().ToLower().Trim();
if (stateName == string.Empty)
return;
state = WorkflowInit.Runtime
.GetAvailableStateToSet(processId.Value, Thread.CurrentThread.CurrentCulture)
.Where(c => c.Name.Trim().ToLower() == stateName).FirstOrDefault();
if (state == null)
Console.WriteLine("The state isn't found.");
else
break;
} while (true);
if (state != null)
{
WorkflowInit.Runtime.SetState(processId.Value, string.Empty, string.Empty,
state.Name, new Dictionary<string, object>());
Console.WriteLine("SetState - OK.", processId);
}
}
private static void DeleteProcess()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
WorkflowInit.Runtime.DeleteInstance(processId.Value);
Console.WriteLine("DeleteProcess - OK.", processId);
processId = null;
}
}
}
View the complete structure of your solution, illustrating the relationships between the ASP.NET Core MVC web application, the workflow designer, and the console application
With these components in place, run our console application to interact with the workflow engine. Execute commands, create instances, and observe the workflow processes in action.
By completing these steps, we've integrated a workflow engine with an application that capable of managing processes and executing commands, it's offering a practical demonstration of the power of workflow automation.
Thank you for joining me on this journey through the intricacies of setting up a workflow engine with WorkflowEngine.io. I hope this article proves to be a valuable resource for you. Best of luck with your workflow endeavors, and until next time, happy coding! :3