Create a Simple .NET Workflow App From Scratch – Your Ultimate Guide
Tags: dotnet, react, csharp, tutorial
Published: 2024-05-20 09:29:14

#Introduction:

At Optimajet, we have long planned to start our own blog on dev.to to share what we believe are useful tutorials, news, articles, and other valuable content with the developer community. And now, it has finally happened!

We decided that many might find it interesting and beneficial to learn and understand how to create an application from scratch using the Community Version of the Optimajet Workflow Engine.

In this guide we will explain how to integrate Workflow Engine with React. We will create two applications:
The guide turned out to be quite extensive, so we divided it into two parts.

**Part 1 - Backend - an ASP.NET application with Workflow Engine and Controllers.**

**Part 2 - Frontend - a React application with Workflow Designer and simple admin panel.**

The first part in this post covers creating the Backend – an ASP.NET application with Workflow Engine and Controllers.

We will publish the second part in the next post, and it will cover creating the Frontend – a React application with a Workflow Designer and a simple admin panel.

First of all, we will need to download the Optimajet Workflow Engine. You can easily download the Community Version of the [Optimajet Workflow Engine from GitHub](https://github.com/optimajet/WorkflowEngine.NET).

If you need a commercial license or are working on a project for a university course or a startup, you can download a version from the [official website and request licensing](https://optimajet.com/products/workflowengine/price/?utm_source=devto) prices. Optimajet supports students by offering educational licenses for free and also helps startups achieve their goals with special discounts.

**SOURCE CODE**

**none** - **initial branch**
[**main**](https://github.com/optimajet/sample-react-example/tree/main) - **final branch**
**none** - **pull request with code changes**

**We will create a process scheme that will do the following:**

1. Run and wait for the user to execute a command `GetWeatherForecast` to receive a weather forecast. Getting the weather forecast will be done with a custom action.
2. After receiving the weather forecast, the process will wait for a command `SendWeatherForecast` from the user to send the weather forecast by e-mail.
3. After the email is sent, the process ends and can be restarted with the `ReRun` command.

To get the weather forecast we will use [Open-Meteo API](https://open-meteo.com/en/docs#api-documentation target="_blank").

We will also restrict the execution of commands:

1. The `GetWeatherForecast` command can be executed by users with the 'User' role.
2. The `SendWeatherForecast` command can be executed by users with the 'Manager' role.
3. The `ReRun` command can be executed by users without the 'User' role.

{% youtube Tw4klz27X9U %} 

## Prerequisites

1. [Docker.](https://www.docker.com/products/docker-desktop/)
2. [.NET 6](https://dotnet.microsoft.com/en-us/download/dotnet/6.0).
3. [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio](https://visualstudio.microsoft.com/).
4. [NodeJS](https://nodejs.org/en/).
5. [JetBrains IDEA](https://www.jetbrains.com/idea/) or [Visual Studio Code](https://code.visualstudio.com/) or another tool to edit JavaScript code.
Console.

## Create a database for the Workflow Engine

First, we need a database. We will use the docker container [azure-sql-edge](https://hub.docker.com/_/microsoft-azure-sql-edge) because it works on ARM64.

Open the console and run the following commands to start the database:

```bash
docker pull mcr.microsoft.com/azure-sql-edge:latest
docker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=StrongPassword#1' -p 1433:1433 --name azuresqledge -d mcr.microsoft.com/azure-sql-edge
```

We now have a **master** database with login **sa** and password **StrongPassword#1** running on port **1433**.

Now connect to the database with your favorite tool and execute [SQL script](https://github.com/optimajet/WorkflowEngine.NET/blob/master/Providers/OptimaJet.Workflow.DbPersistence/SQL/CreatePersistenceObjects.sql) .

## Create a backend application

OK. Now we have a database. Let's create a backend application. To create a backend application, we will create a .NET 6 solution. Then add two projects for this solution:

1. WorkflowLib is the library that contains the Workflow Engine.
2. WorkflowApi is an ASP.NET project with an API for the Workflow Engine.

It's all covered in the [How to integrate guide](https://workflowengine.io/documentation/how-to-integrate), so let's just copy and paste with different project names:

Let's create a folder for our example and go into it:

```bash
mkdir react-example
cd react-example
```
Then create a `Backend` folder and go into it, create an empty solution and add the WorkflowLib library to the solution. Also add a `WorkflowApi` MVC project and add it to the solution:

```bash
mkdir Backend
cd Backend
dotnet new sln
dotnet new classlib --name WorkflowLib
dotnet sln add WorkflowLib
dotnet new mvc --name WorkflowApi
dotnet sln add WorkflowApi
dotnet add WorkflowApi reference WorkflowLib
```
Great, now we have two projects: `WorkflowLib` and `WorkflowApi`. The `WorkflowApi` project references the `WorkflowLib` project. Let's add the dependencies for the nuget Workflow Engine packages to these projects:

1. [WorkflowEngine.NETCore-Core](https://www.nuget.org/packages/WorkflowEngine.NETCore-Core/).
2. [WorkflowEngine.NETCore-ProviderForMSSQL](https://www.nuget.org/packages/WorkflowEngine.NETCore-ProviderForMSSQL/).

```bash
dotnet add WorkflowLib package WorkflowEngine.NETCore-Core
dotnet add WorkflowLib package WorkflowEngine.NETCore-ProviderForMSSQL
dotnet add WorkflowApi package WorkflowEngine.NETCore-Core
dotnet add WorkflowApi package WorkflowEngine.NETCore-ProviderForMSSQL
```
**Setting a port for the backend**

Now it's time to open the `Backend` solution in your favorite IDE. We will be using JetBrains Rider.

Now we will change the port for running the backend to 5139, since the dotnet CLI can install another port during the project generation process. Open the `launchSettings.json` file in the `Properties` folder of the `WorkflowApi` project and enter the value `http://localhost:5139`, as in the example below:

Backend/WorkflowApi/Properties/launchSettings.json
```json
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:25849",
      "sslPort": 44372
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true, 
      "applicationUrl": "http://localhost:5139",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7228;http://localhost:5139",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

**Create a simple user model**

In our tutorial, we will use a simple user model. There will be four users: Peter, Margaret, John and Sam. And only two roles: User and Manager. To describe a `user`, we'll use a simple User class containing the user's name and a list of their roles.

Let's add the `User` class to our `WorkflowLib` project:

Backend/WorkflowLib/User.cs

```csharp
namespace WorkflowLib;

public class User
{
    public string Name { get; set; }
    public List<string> Roles { get; set; }
}
```
Next, add a `Users` class containing our users:

Backend/WorkflowLib/Users.cs

```csharp
namespace WorkflowLib;

public static class Users
{
    public static readonly List<User> Data = new()
    {
        new User {Name = "Peter", Roles = new List<string> {"User", "Manager"}},
        new User {Name = "Margaret", Roles = new List<string> {"User"}},
        new User {Name = "John", Roles = new List<string> {"Manager"}},
        new User {Name = "Sam", Roles = new List<string> {"Manager"}},
    };

    public static readonly Dictionary<string, User> UserDict = Data.ToDictionary(u => u.Name);
}
```
To get the user by their name, we will use the `UserDict` field.

**Connecting WorkflowLib to Workflow Engine**

First, remove the `Class1` class from the `WorkflowLib` project. We just don't need this class generated by the dotnet CLI.

Second, add `WorkflowInit` class, like in How to integrate tutorial. We will use a slightly modified example of this class now:

Backend/WorkflowLib/WorkflowInit.cs

```csharp
using System.Xml.Linq;
using OptimaJet.Workflow.Core;
using OptimaJet.Workflow.Core.Builder;
using OptimaJet.Workflow.Core.Parser;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.DbPersistence;
using OptimaJet.Workflow.Plugins;

namespace WorkflowLib;

public static class WorkflowInit
{
    private const string ConnectionString = "Data Source=(local);Initial Catalog=master;User ID=sa;Password=StrongPassword#1";

    private static readonly Lazy<WorkflowRuntime> LazyRuntime = new(InitWorkflowRuntime);
    private static readonly Lazy<MSSQLProvider> LazyProvider = new(InitMssqlProvider);

    public static WorkflowRuntime Runtime => LazyRuntime.Value;
    public static MSSQLProvider Provider => LazyProvider.Value;

    private static MSSQLProvider InitMssqlProvider()
    {
        return new MSSQLProvider(ConnectionString);
    }

    private static WorkflowRuntime InitWorkflowRuntime()
    {
        // TODO If you have a license key, you have to register it here
        //WorkflowRuntime.RegisterLicense("your license key text");

        var builder = new WorkflowBuilder<XElement>(
            Provider,
            new XmlWorkflowParser(),
            Provider
        ).WithDefaultCache();

        // we need BasicPlugin to send email
        var basicPlugin = new BasicPlugin
        {
            Setting_MailserverFrom = "mail@gmail.com",
            Setting_Mailserver = "smtp.gmail.com",
            Setting_MailserverSsl = true,
            Setting_MailserverPort = 587,
            Setting_MailserverLogin = "mail@gmail.com",
            Setting_MailserverPassword = "Password"
        };
        var runtime = new WorkflowRuntime()
            .WithPlugin(basicPlugin)
            .WithBuilder(builder)
            .WithPersistenceProvider(Provider)
            .EnableCodeActions()
            .SwitchAutoUpdateSchemeBeforeGetAvailableCommandsOn()
            // add custom activity
            .WithCustomActivities(new List<ActivityBase> {new WeatherActivity()})
            // add custom rule provider
            .WithRuleProvider(new SimpleRuleProvider())
            .AsSingleServer();

        // events subscription
        runtime.OnProcessActivityChangedAsync += (sender, args, token) => Task.CompletedTask;
        runtime.OnProcessStatusChangedAsync += (sender, args, token) => Task.CompletedTask;

        runtime.Start();

        return runtime;
    }
}
```
With the `WorkflowInit` class, we can access the [WorkflowRuntime](https://workflowengine.io/documentation/main-terms/runtime) with the `WorkflowInit.Runtime` statement and the `MSSQLProvider` database provider with the `WorkflowInit.Provider` statement.

We have also initialized the `BasicPlugin` for sending email, fill in its properties with your settings. We also added a custom activity `WeatherActivity` and a rule provider `SimpleRuleProvider`. These classes will be added later.

**Adding a custom activity**

The process of adding a custom activity is described [here](https://workflowengine.io/documentation/designer-customization/custom-activity). We'll add a custom activity called `WeatherActivity` that gets the weather forecast and stores the result in process variables.

Add the `WeatherActivity` class to the `WorkflowLib` project:

Backend/WorkflowLib/WeatherActivity.cs

```csharp
using System.Net;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OptimaJet.Workflow.Core;
using OptimaJet.Workflow.Core.Model;
using OptimaJet.Workflow.Core.Runtime;

namespace WorkflowLib;

public sealed class WeatherActivity : ActivityBase
{
    public WeatherActivity()
    {
        Type = "WeatherActivity";
        Title = "Weather forecast";
        Description = "Get weather forecast via API";
        
        // the file name with your form template, without extension
        Template = "weatherActivity";
        // the file name with your svg template, without extension
        SVGTemplate = "weatherActivity";
    }

    public override async Task ExecutionAsync(WorkflowRuntime runtime, ProcessInstance processInstance,
        Dictionary<string, string> parameters, CancellationToken token)
    {
        const string url = "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&daily=temperature_2m_min&timezone=GMT";
        using var httpClient = new HttpClient();
        var response = await httpClient.GetAsync(url, token);
        if (response.StatusCode == HttpStatusCode.OK)
        {
            await using var stream = await response.Content.ReadAsStreamAsync(token);
            using var streamReader = new StreamReader(stream);
            var data = await streamReader.ReadToEndAsync();
            var json = JsonConvert.DeserializeObject<JObject>(data);
            var daily = json?["daily"];
            var date = daily?["time"]?[0] ?? "Date is not defined";
            var temperature = daily?["temperature_2m_min"]?[0] ?? "Temperature is not defined";

            // store the entire response to the Weather process parameter
            await processInstance.SetParameterAsync("Weather", json, ParameterPurpose.Persistence);
            // store the weather date in the WeatherDate process parameter
            await processInstance.SetParameterAsync("WeatherDate", date, ParameterPurpose.Persistence);
            // store the temperature in the process parameter WeatherTemperature
            await processInstance.SetParameterAsync("WeatherTemperature", temperature, ParameterPurpose.Persistence);
        }
    }

    public override async Task PreExecutionAsync(WorkflowRuntime runtime, ProcessInstance processInstance,
        Dictionary<string, string> parameters, CancellationToken token)
    {
        // do nothing
    }
}
```
All code is in the `ExecutionAsync` method. This method will be executed when the activity becomes active.

**Adding RuleProvider**

To restrict the execution of transitions, we need to implement the `IWorkflowRuleProvider` interface. You can read more about Rules [here](https://workflowengine.io/documentation/scheme/rules).

In this tutorial, we will create a simple implementation of the `IWorkflowRuleProvider` interface that will check user roles.

Add the `SimpleRuleProvider` class to the WorkflowLib project:

Backend/WorkflowLib/SimpleRuleProvider.cs

```csharp
using OptimaJet.Workflow.Core.Model;
using OptimaJet.Workflow.Core.Runtime;

namespace WorkflowLib;

public class SimpleRuleProvider : IWorkflowRuleProvider
{
    // name of our rule
    private const string RuleCheckRole = "CheckRole";

    public List<string> GetRules(string schemeCode, NamesSearchType namesSearchType)
    {
        return new List<string> {RuleCheckRole};
    }
    
    public bool Check(ProcessInstance processInstance, WorkflowRuntime runtime, string identityId, string ruleName, string parameter)
    {
        // we check that the identityId satisfies our rule, that is, the user has the role specified in the parameter
        
        if (RuleCheckRole != ruleName || identityId == null || !Users.UserDict.ContainsKey(identityId)) return false;

        var user = Users.UserDict[identityId];
        return user.Roles.Contains(parameter);
    }

    public async Task<bool> CheckAsync(ProcessInstance processInstance, WorkflowRuntime runtime, string identityId, string ruleName,
        string parameter,
        CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public IEnumerable<string> GetIdentities(ProcessInstance processInstance, WorkflowRuntime runtime, string ruleName, string parameter)
    {
        // return all identities (the identity is user name)
        return Users.Data.Select(u => u.Name);
    }

    public async Task<IEnumerable<string>> GetIdentitiesAsync(ProcessInstance processInstance, WorkflowRuntime runtime, string ruleName,
        string parameter,
        CancellationToken token)
    {
        throw new NotImplementedException();
    }

    public bool IsCheckAsync(string ruleName, string schemeCode)
    {
        // use the Check method instead of CheckAsync
        return false;
    }

    public bool IsGetIdentitiesAsync(string ruleName, string schemeCode)
    {
        // use the GetIdentities method instead of GetIdentitiesAsync
        return false;
    }
}
```
The `SimpleRuleProvider` class is quite simple, check the comments in the code.

**Connecting `WorkflowApi` with Designer controller**

The next thing we need is the Designer controller. This controller will respond to Workflow Designer HTTP requests. Let's copy and paste controller code from the tutorial [How to integrate](https://workflowengine.io/documentation/how-to-integrate#designer) into the WorkfowApi project in the `Controller` folder:

Backend/WorkflowApi/Controllers/DesignerController.cs

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

namespace WorkflowApi.Controllers;

public class DesignerController : Controller
{
    public async Task<IActionResult> Api()
    {
        Stream? filestream = null;
        var parameters = new NameValueCollection();

        //Defining the request method
        var isPost = Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase);

        //Parse the parameters in the query string
        foreach (var q in Request.Query)
        {
            parameters.Add(q.Key, q.Value.First());
        }

        if (isPost)
        {
            //Parsing the parameters passed in the form
            var keys = parameters.AllKeys;

            foreach (var key in Request.Form.Keys)
            {
                if (!keys.Contains(key))
                {
                    parameters.Add(key, Request.Form[key]);
                }
            }

            //If a file is passed
            if (Request.Form.Files.Count > 0)
            {
                //Save file
                filestream = Request.Form.Files[0].OpenReadStream();
            }
        }

        //Calling the Designer Api and store answer
        var (result, hasError) = await WorkflowInit.Runtime.DesignerAPIAsync(parameters, filestream);

        //If it returns a file, send the response in a special way
        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");

        //response
        return Content(result);
    }
}
```
Let's check that the solution works now. Run this command in a shell:

```bash
dotnet run --project WorkflowApi
```
And you should see something like this in your console:

```bash
Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5139
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/aksanaazarenka/projects/optimajet/temp/react-example/Backend/WorkflowApi/
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.
```
You could open your browser at [http://localhost:5139](http://localhost:5139) (in your case address may be different) and see usual ASP.NET MVC welcome page. Now stop the application (Ctrl+C).

**Adding CORS**

Since we will be running our frontend application at a different address, we need to add CORS to the WorkflowApi project. To do this, we need to modify the Program class by adding the highlighted code, for example:

Backend/WorkflowApi/Program.cs

```csharp
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

const string rule = "MyCorsRule";                               
builder.Services.AddCors(options =>                             
{                                                               
    options.AddPolicy(rule, policy => policy.AllowAnyOrigin()); 
});                                                             

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseCors(rule);

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```
**Creating a user controller**

Let's add a controller to display all users via the HTTP API. To do this, we need to create a simple `UserController` controller class. in the `WorkflowApi` project in the Controllers folder:

Backend/WorkflowApi/Controllers/UserController.cs

```
using Microsoft.AspNetCore.Mvc;
using WorkflowLib;

namespace WorkflowApi.Controllers;

[Route("api/user")]
public class UserController : ControllerBase
{
    [HttpGet]
    [Route("all")]
    public async Task<IActionResult> GetUsers()
    {
        return Ok(Users.Data);
    }
}
```
**Testing user controller**

Now we want to make sure that users are returned via the HTTP API. Let's start our application:

```bash
dotnet run --project WorkflowApi
```
And open your browser at [http://localhost:5139/api/user/all](http://localhost:5139/api/user/all).

You should see something similar to this JSON:

```json
[
  {
    "name": "Peter",
    "roles": [
      "User",
      "Manager"
    ]
  },
  {
    "name": "Margaret",
    "roles": [
      "User"
    ]
  },
  {
    "name": "John",
    "roles": [
      "Manager"
    ]
  },
  {
    "name": "Sam",
    "roles": [
      "Manager"
    ]
  }
]
```
Now stop the application (Ctrl+C).

**Create DTO classes**

Since we want to control the workflow with a React app, we need to add a controller to the backend that we can call later. In that controller, we will add methods to get schemas and process instances. We will need methods to execute commands and get the current status of the process instance. We also need DTO classes that will be sent via the HTTP API.

To describe the [scheme](https://workflowengine.io/documentation/scheme) of the process, we will use a simple DTO class `WorkflowSchemeDt`o with just two properties:

1. `Code` - scheme code. This is a unique scheme code and is used when you need to start a process.
2. `Tags` - scheme tags. You can read more about tags in our [documentation](https://workflowengine.io/documentation/scalability/multitenancy#tags).

The `WorkflowSchemeDto` class is a partial copy of [SchemeEntity](https://workflowengine.io/documentation/api-reference/OptimaJet.Workflow.Core.Entities/SchemeEntity) class. Add the `WorkflowSchemeDto` class to the `WorkflowApi` project:

Backend/WorkflowApi/Models/WorkflowSchemeDto.cs

```csharp
namespace WorkflowApi.Models;

public class WorkflowSchemeDto
{
    public string Code { get; set; }
    public string Tags { get; set; }
}
```
To describe process instances, we will use the `WorkflowProcessDto` class with the following properties:

1. Id - unique identifier of the process.
2. Scheme - scheme code, the value is the same as in the 
3. `WorkflowSchemeDto.Code` property.
4. StateName - process state.
5. ActivityName - current activity name.
6. CreationDate - the date and time the process was created.

Add the `WorkflowProcessDto` class to the `WorkflowApi` project:

Backend/WorkflowApi/Models/WorkflowProcessDto.cs

```csharp
namespace WorkflowApi.Models;

public class WorkflowProcessDto
{
    public string Id { get; set; }
    public string Scheme { get; set; }
    public string StateName { get; set; }
    public string ActivityName { get; set; }
    public string CreationDate { get; set; }
}
```
To describe the available commands for a process instance, we will use another DTO named `WorkflowProcessCommandsDto`. It will contain the following properties:

1. Id - unique identifier of the process.
2. Commands - list of available commands for the process instance.

Add the `WorkflowProcessCommandsDto` class to the `WorkflowApi` project:

Backend/WorkflowApi/Models/WorkflowProcessCommandsDto.cs

```csharp
namespace WorkflowApi.Models;

public class WorkflowProcessCommandsDto
{
    public string Id { get; set; }
    public List<string> Commands { get; set; }
}
```
**Create workflow controller**

Now we have all the DTO classes, it's time to add a controller that will serve the data from the Workflow Engine. This controller will have several methods:

1. Schemes - returns workflow schemes.
2. Instances - returns process instances.
3. CreateInstance - to create an instance of the process.
4. Commands - returns the available commands of the process instance.
5. ExecuteCommand - to execute the process instance command.

For the simplicity we will use [MSSQLProvider](https://github.com/optimajet/WorkflowEngine.NET/blob/master/Providers/OptimaJet.Workflow.DbPersistence/Source/MSSQLProvider.cs) to fetch the data from the database. This class has basic functionality, if you want to create complex database queries, for example JOIN, you'd better use something like [Entity Framework](https://learn.microsoft.com/en-us/ef/).

Let's add the `WorkflowController` class to the `WorkflowApi` project. To understand how this class works, read the comments in the code.

Backend/WorkflowApi/Controllers/WorkflowController.cs

```csharp
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using OptimaJet.Workflow.Core.Entities;
using OptimaJet.Workflow.Core.Persistence;
using WorkflowApi.Models;
using WorkflowLib;

namespace WorkflowApi.Controllers;

[Route("api/workflow")]
public class WorkflowController : ControllerBase
{
    /// <summary>
    /// Returns process schemes from the database
    /// </summary>
    /// <returns>Process schemes</returns>
    [HttpGet]
    [Route("schemes")]
    public async Task<IActionResult> Schemes()
    {
        // getting a connection to the database
        await using var connection = WorkflowInit.Provider.OpenConnection();
        // creating parameters for the "ORDER BY" clause
        var orderParameters = new List<(string parameterName, SortDirection sortDirection)>
        {
            ("Code", SortDirection.Asc)
        };
        // creating parameters for the "LIMIT" and "OFFSET" clauses
        var paging = Paging.Create(0, 200);
        // getting schemes from the database
        var list = await WorkflowInit.Provider.WorkflowScheme
            .SelectAllWorkflowSchemesWithPagingAsync(connection, orderParameters, paging);

        // converting schemes to DTOs
        var results = list.Select(s => new WorkflowSchemeDto
        {
            Code = s.Code,
            Tags = s.Tags
        });
        return Ok(results);
    }

    /// <summary>
    /// Returns process instances from the database
    /// </summary>
    /// <returns>Process instances</returns>
    [HttpGet]
    [Route("instances")]
    public async Task<IActionResult> Instances()
    {
        // getting a connection to the database
        await using var connection = WorkflowInit.Provider.OpenConnection();
        // creating parameters for the "ORDER BY" clause
        var orderParameters = new List<(string parameterName, SortDirection sortDirection)>
        {
            ("CreationDate", SortDirection.Desc)
        };
        // creating parameters for the "LIMIT" and "OFFSET" clauses
        var paging = Paging.Create(0, 200);
        // getting process instances from the database
        var processes = await WorkflowInit.Provider.WorkflowProcessInstance
            .SelectAllWithPagingAsync(connection, orderParameters, paging);

        // converting process instances to DTOs
        var result = processes.Select(async p =>
            {
                // getting process scheme from another table to get SchemeCode
                var schemeEntity = await WorkflowInit.Provider.WorkflowProcessScheme.SelectByKeyAsync(connection, p.SchemeId!);
                // converting process instances to DTO
                return ConvertToWorkflowProcessDto(p, schemeEntity.SchemeCode);
            })
            .Select(t => t.Result)
            .ToList();

        return Ok(result);
    }

    /// <summary>
    /// Creates a process instance for the process scheme
    /// </summary>
    /// <param name="schemeCode">Process scheme code</param>
    /// <returns>Process instance</returns>
    [HttpGet]
    [Route("createInstance/{schemeCode}")]
    public async Task<IActionResult> CreateInstance(string schemeCode)
    {
        // generating a new processId
        var processId = Guid.NewGuid();
        // creating a new process instance
        await WorkflowInit.Runtime.CreateInstanceAsync(schemeCode, processId);

        // getting a connection to the database
        await using var connection = WorkflowInit.Provider.OpenConnection();
        // getting process instance from the database
        var processInstanceEntity = await WorkflowInit.Provider.WorkflowProcessInstance
            .SelectByKeyAsync(connection, processId);

        // converting process instances to DTO
        var workflowProcessDto = ConvertToWorkflowProcessDto(processInstanceEntity, schemeCode);
        return Ok(workflowProcessDto);
    }

    /// <summary>
    /// Returns process instance commands
    /// </summary>
    /// <param name="processId">Unique process identifier</param>
    /// <param name="identityId">Command executor identifier</param>
    /// <returns></returns>
    [HttpGet]
    [Route("commands/{processId:guid}/{identityId}")]
    public async Task<IActionResult> Commands(Guid processId, string identityId)
    {
        // getting a process instance and its parameters
        var process = await WorkflowInit.Runtime.GetProcessInstanceAndFillProcessParametersAsync(processId);
        // getting available commands for a process instance
        var commands = await WorkflowInit.Runtime.GetAvailableCommandsAsync(processId, identityId);
        // convert process instance commands to a list of strings
        var commandNames = commands?.Select(c => c.CommandName).ToList() ?? new List<string>();
        // creating the resulting DTO
        var dto = new WorkflowProcessCommandsDto
        {
            Id = process.ProcessId.ToString(),
            Commands = commandNames
        };
        return Ok(dto);
    }

    /// <summary>
    /// Executes a command on a process instance
    /// </summary>
    /// <param name="processId">Unique process identifier</param>
    /// <param name="command">Command</param>
    /// <param name="identityId">Command executor identifier</param>
    /// <returns>true if the command was executed, false otherwise</returns>
    [HttpGet]
    [Route("executeCommand/{processId:guid}/{command}/{identityId}")]
    public async Task<IActionResult> ExecuteCommand(Guid processId, string command, string identityId)
    {
        // getting available commands for a process instance
        var commands = await WorkflowInit.Runtime.GetAvailableCommandsAsync(processId, identityId);
        // search for the necessary command
        var workflowCommand = commands?.First(c => c.CommandName == command)
                              ?? throw new ArgumentException($"Command {command} not found");
        // executing the command
        var result = await WorkflowInit.Runtime.ExecuteCommandAsync(workflowCommand, identityId, null);
        return Ok(result.WasExecuted);
    }

    /// <summary>
    /// Converts ProcessInstanceEntity to WorkflowProcessDto
    /// </summary>
    /// <param name="processInstanceEntity">Process instance entity</param>
    /// <param name="schemeCode">Scheme code</param>
    /// <returns>WorkflowProcessDto</returns>
    private static WorkflowProcessDto ConvertToWorkflowProcessDto(ProcessInstanceEntity processInstanceEntity, string schemeCode)
    {
        var workflowProcessDto = new WorkflowProcessDto
        {
            Id = processInstanceEntity.Id.ToString(),
            Scheme = schemeCode,
            StateName = processInstanceEntity.StateName,
            ActivityName = processInstanceEntity.ActivityName,
            CreationDate = processInstanceEntity.CreationDate.ToString(CultureInfo.InvariantCulture)
        };
        return workflowProcessDto;
    }
}
```
**Testing workflow controller**

Now let's check that the controller is working. Run this command in a shell:

```bash
dotnet run --project WorkflowApi
```
Then open your browser at [http://localhost:5139/api/workflow/schemes](http://localhost:5139/api/workflow/schemes). Then open your browser at [http://localhost:5139/api/workflow/instances](http://localhost:5139/api/workflow/instances). In both pages you should see an empty JSON array in response:

```json
[]
```
That's OK because we don't have any process schemes or process instances in our database. Stop the application (Ctrl+C).

The continuation of this article, where we will create a React web application, can be read in the second part.

**P.S.**

_We are taking our first steps in creating content that is useful to the developer community, so we ask you not to judge us too harshly and to help us with advice, comments, and recommendations on how to improve our publications. We welcome any feedback in the comments._