DEV Community

Muhammad Umar Al Fajar
Muhammad Umar Al Fajar

Posted on

Workflow Engine Integration in .NET 8

In general, a workflow is a series of activities required to complete a task. In a business context, a workflow refers to a systematic work process where tasks completed by one party are passed on to another party for further action according to certain agreed-upon procedures within the company.

This workflow can be implemented in .NET using various available tools. Some examples of these tools that already support .NET include Workflow Engine and Elsaworkflows. On this occasion, we will implement Workflow using Workflow Engine as an example.


Workflow Engine Integration

To begin integrating .NET with Workflow Engine, make sure that all of the following technologies are installed on your computer:

  1. Visual Studio 2022 Community Edition
  2. .NET 8
  3. MS SQL Server

The steps involved in integrating .NET with Workflow Engine are as follows:

  • Create a blank solution

blank solution

  • After the project was created, add new project by right click in the solution then Add > New Project. Choose Class Library and name it WorkflowLib.

workflow lib

  • Add another project with the same step but now choose ASP.NET Core Web App (Model-View-Controller), name it DesignerApp.

designer app


Database Preparation

  • Download the .NET Core persistence provided by Workflow Engine here.

  • Extract the zip file in the folder WorkflowEngine.NET-12.1.1 > Providers > OptimaJet.Workflow.DbPersistence.

  • After the folder is extracted, there will be a folder named SQL.

  • Back to the solution, create a new folder called SQL.

  • Add the files to the SQL folder by right-clicking on Solution > Add > Existing Item. Then select all the files in the SQL folder from the previously extracted folder.

add sql

  • Before executing the SQL file, first create a new database in MS SQL Server and name it WorkflowEngineDb.

  • After that connect to the database.

connect to database

  • Then fill in the Server Name and Database Name according to what you have created previously. If the database is not found, try changing Trust Server Certificate to True.

select the server

  • Back to Visual Studio 2022, open the file CreatePersistanceObjects.sql and click the following button.

execute button

  • After the file was executed, a display like this will appear:

execute success

Package Preparation

Before starting the integration, there are some packages that need to be added, as follows:

  1. WorkflowEngine.NETCore-Core
  2. WorkflowEngine.NETCore-ProviderForMSSQL

To add the packages above, you can use the following method:

  • Open Tools > NuGet Package Manager > Manage NuGet Packages for Solution.
  • Select Browse and enter the keyword according to the package name above.
  • Don't forget to check both projects that were created earlier when installing the package.

package to install

WorkflowRuntime Initiation
  • Open the WorkflowLib project.
  • Create a new class named WorkflowInit.cs.
  • Add and adjust the following code according to the project you created.


using System.Xml.Linq;
using OptimaJet.Workflow.Core.Builder;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.DbPersistence;

namespace WorkflowLib
{
    public static class WorkflowInit
    {
        [Obsolete]
        private static readonly Lazy<WorkflowRuntime> LazyRuntime = new Lazy<WorkflowRuntime>(InitWorkflowRuntime);

        [Obsolete]
        public static WorkflowRuntime Runtime
        {
            get { return LazyRuntime.Value; }
        }

        public static string ConnectionString { get; set; }

        [Obsolete]
        private static WorkflowRuntime InitWorkflowRuntime()
        {
            // TODO Uncomment for .NET Framework if you don't set ConnectionString externally.
            //ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;

            if (string.IsNullOrEmpty(ConnectionString))
            {
                throw new Exception("Please init ConnectionString before calling the Runtime!");
            }
            // TODO If you have a license key, you have to register it here
            //WorkflowRuntime.RegisterLicense("your license key text");

            // TODO If you are using database different from SQL Server you have to use different persistence provider here.
            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();
            // Settings for SendEmail actions
            // plugin.Setting_Mailserver = "smtp.yourserver.com";
            // plugin.Setting_MailserverPort = 25;
            // plugin.Setting_MailserverFrom = "from@yourserver.com";
            // plugin.Setting_MailserverLogin = "login@yourserver.com";
            // plugin.Setting_MailserverPassword = "pass";
            // plugin.Setting_MailserverSsl = true;
            runtime.WithPlugin(plugin);

            // events subscription
            runtime.ProcessActivityChanged += (sender, args) => { };
            runtime.ProcessStatusChanged += (sender, args) => { };
            // TODO If you have planned to use Code Actions functionality that required references to external assemblies
            // you have to register them here
            //runtime.RegisterAssemblyForCodeActions(Assembly.GetAssembly(typeof(SomeTypeFromMyAssembly)));

            // starts the WorkflowRuntime
            // TODO If you have planned use Timers the best way to start WorkflowRuntime is somewhere outside
            // of this function in Global.asax for example
            runtime.Start();

            return runtime;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode
Connect the MVC with Lib
  • Open the DesignerApp
  • Right click on the project, then Add > Project Reference
  • Check the WorkflowLib and click OK.
Project Designer Initiation
  • Open the DesignerApp project.

  • Right-click on the Controllers folder. Select Add > Controller and name it Designer. After the Controller is added, a new class named DesignerController.cs will appear.

  • Open DesignerController.cs.

  • Right-click on the View() method and select Add View.

  • Then select Razor View - Empty. Then name it Index.cshtml.

  • After the View is added, a new folder and file will appear in the Views folder.

  • Open the Index.cshtml file and add the following code.



@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 () {
        // This function is anonymous, is executed immediately and
        // the return value is assigned to QueryString!
        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 first entry with this name
            if (typeof query_string[pair[0]] === "undefined") {
                query_string[pair[0]] = pair[1];
                // If second entry with this name
            } else if (typeof query_string[pair[0]] === "string") {
                var arr = [query_string[pair[0]], pair[1]];
                query_string[pair[0]] = arr;
                // If third or later entry with this name
            } else {
                query_string[pair[0]].push(pair[1]);
            }
        }
        return query_string;
    }();
    //Load settings
    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>


Enter fullscreen mode Exit fullscreen mode
  • In addition, add and adjust the following code to the DesignerController.cs class.


using System.Collections.Specialized;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using OptimaJet.Workflow;
using WorkflowLib;

namespace DesignerApp.Controllers
{
    public class DesignerController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [Obsolete]
        public async Task<IActionResult> Api()
        {
            Stream? filestream = null;
            var isPost = Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase);
            if (isPost && Request.Form.Files != null && Request.Form.Files.Count > 0)
                filestream = Request.Form.Files[0].OpenReadStream();

            var pars = new NameValueCollection();
            foreach (var q in Request.Query)
            {
                pars.Add(q.Key, q.Value.First());
            }


            if (isPost)
            {
                var parsKeys = pars.AllKeys;
                //foreach (var key in Request.Form.AllKeys)
                foreach (string key in Request.Form.Keys)
                {
                    if (!parsKeys.Contains(key))
                    {
                        pars.Add(key, Request.Form[key]);
                    }
                }
            }

            (string res, bool hasError) = await WorkflowInit.Runtime.DesignerAPIAsync(pars, filestream);

            var operation = pars["operation"]?.ToLower();
            if (operation == "downloadscheme" && !hasError)
                return File(Encoding.UTF8.GetBytes(res), "text/xml");
            else if (operation == "downloadschemebpmn" && !hasError)
                return File(UTF8Encoding.UTF8.GetBytes(res), "text/xml");

            return Content(res);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode
  • After that, download the workflowdesigner.min.css and workflowdesigner.min.js files from GitHub. Then copy and paste the two files into the project in the css and js folders as follows.

paste location

  • Also add and adjust the site.css file to be as follows to decorate the designer view.


html {
  font-size: 14px;
}

@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

html {
  position: relative;
  min-height: 100%;
}

body {
    margin-bottom: 60px;
}

a {
    outline: none;
    text-decoration: none;
}

.ui {
    padding: 8px 15px;
}

.ui.primary.button,
.ui.primary.button:focus {
    background: #f2f2f2;
    border: 1px solid #f2f2f2;
    border-radius: 2px;
    font-weight: normal;
    color: #2c2c2c;
}

    .ui.primary.button:hover {
        background: #c9c9c9;
    }

    .ui.primary.button:active {
        background: #b5b5b5;
    }

.ui.secondary.button,
.ui.secondary.button:focus {
    background: #FFFFFF;
    border: 1px solid #f2f2f2;
    border-radius: 2px;
    font-weight: normal;
    color: #4d4d4d;
}

.ui.secondary.button:hover {
    background: #dbdbdb;
    color: #2c2c2c;
}

.ui.secondary.button:active {
    background: #dbdbdb;
    color: #2c2c2c;
}


Enter fullscreen mode Exit fullscreen mode
Connect to Database
  • Open appsettings.json in the root project DesignerApp.

  • Add ConnectionStrings according to the database on each device, for example as follows.



{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "data source=mumarual\\SQLEXPRESS;initial catalog=WorkflowEngineDb;trusted_connection=true;TrustServerCertificate=true"
  },
  "AllowedHosts": "*"
}


Enter fullscreen mode Exit fullscreen mode
  • Then add the following code to Program.cs to set the default ConnectionString for the WorkflowInit.cs class.

WorkflowInit.ConnectionString = app.Configuration.GetConnectionString("DefaultConnection");

Run The Project

Before running the project, to make it easier to access the designer, we will add a new button to the application's navbar.

  • Open the Views > Shared folder. Select _Layout.cshtml.

  • Add the Designer controller to the list in the navbar as follows.



<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Designer" asp-action="Index">Workflow Designer</a>
</li>


Enter fullscreen mode Exit fullscreen mode
  • Then try running the project by pressing the play button in Visual Studio 2022.

  • Don't forget to set the startup project as follows to only run DesignerApp.

configure startup project

set the startup

  • Select the Workflow Designer menu on the navbar and the Designer is ready to use.

workflow engine

GitHub

Top comments (1)

Collapse
 
optimajet profile image
Optimajet Limited

@mumaralfajar
Awesome article @mumaralfajar
Thank you for such a detailed piece on WorkflowEngine. Optimajet Workflow Engine technical team highly appreciates the technical level. If any users encounter limitations with the Community version of WorkflowEngine, Optimajet is always ready to accommodate their licensing needs. They just need to contact the sales department and describe their case in the application form available here