Workflow Engine with .NET

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:

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."

Add New Project

Left click Solution > Add > New Project, then add new class library

Searching Class Library

Integrate the required NuGet packages to empower our project:

Once installed, the necessary assemblies will seamlessly augment our project.

Added Packages to 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 New Project

Add Required Packages

Ensure the WorkflowLib is referenced by navigating to Add>ProjectReverence > check mark the WorkflowLib

Add Project Reference

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

  • templates folder directly at wwwroot folder
  • workflowdesigner.min.js on wwwroot/js
  • workflowdesigner.min.css on wwwroot/css

Design Structure

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

Home page

Explore the designer interface and understand how to create workflows.

Designer Page

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.

Add Console Application

then we also need to add project reference to this

Project Reference

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

File structure

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.

Demo

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

要查看或添加评论,请登录

Feri Ginanjar的更多文章

  • Event-Driven ASP.NET Core Microservice Architectures

    Event-Driven ASP.NET Core Microservice Architectures

    Hello again, everyone! ?? It's time to share my experience try to explore a Microservices Web Apps with .NET on this…

  • Full Stack CRUD web API with Angular ASP.NET

    Full Stack CRUD web API with Angular ASP.NET

    Hello again, everyone! ?? It's time to share my experience try exploring create a Web Apps with .NET and Angular…

  • ASP.NET Core MVC with React

    ASP.NET Core MVC with React

    Hello this time I will try to share my exploration make Application ASP.NET Core MVC with React, Prerequisites .

  • Create Simple CRUD with .NET Core and SQL Server

    Create Simple CRUD with .NET Core and SQL Server

    Hello I'm Feri ?? on this article I want to share my experience creating simple CRUD with .NET Core and SQL Server.

社区洞察

其他会员也浏览了