Workflow Engine Integration in .NET 8
Published: 2024-03-25 06:24:55

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](https://visualstudio.microsoft.com/downloads/)
2. [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
3. [MS SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads)

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

* Create a blank solution

![blank solution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7oujyf800ut2kpp8gvcp.png)

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5d9a3f5539wsyghs95kv.png)

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

![designer app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cqdjuddtsif79zc010zg.png)

---

###Database Preparation

* Download the .NET Core persistence provided by Workflow Engine [here](https://github.com/optimajet/WorkflowEngine.NET/releases/tag/12.1.1).

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xvynsdisg6sv6ldxoju.png)

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhuzts1fx4o9n4lshzm9.png)

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmu99910vblr1ykebehf.png)

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

![execute button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rw9qcxhq4icgyh1mgclo.png)

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

![execute success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/31n140sl3e9w1tesu2rt.png)

#####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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a1kn1ir01y67vvnrdbw6.png)

#####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;
        }
    }
}
```

#####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>
```

* 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);
        }
    }
}
```

* After that, download the workflowdesigner.min.css and workflowdesigner.min.js files from [GitHub](https://github.com/optimajet/WorkflowEngine.NET/tree/master/Designer). Then copy and paste the two files into the project in the css and js folders as follows.

![paste location](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjblmbamdz4urmipt9c5.png)

* 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;
}
```

#####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": "*"
}
```

* 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>
```

* 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tw7pq0bxgt3v16yfj4zq.png)

![set the startup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1srowp965ilbicqf1rs0.png)

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

![workflow engine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lfitzwd6rafo3pbtxw87.png)

[GitHub](https://github.com/mumaralfajar/we-net)