Skip to main content

Introducing Formengine - The New Formbuilder, try for FREE formengine.io.

Dependency injection

Tutorial 6 🗃

Source code

states-and-activities - initial branch
dependency injection container - final branch
dependency injection pull request - pull request with code changes

Overview​

In this tutorial we demonstrate how the architectural design of the sample application can be enhanced thought the Dependency injection pattern which allows building applications considering the abstraction approach more than the implementation details. Nowadays, most solutions are written based on compile-time dependency flows in the direction of runtime execution, producing a direct dependency graph, but applying the dependency inversion principle, enables the typical compile-time dependency to be optimized. Therefore, we are going to convert the statical class WorflowInit to WorkflowRuntimeLocator which will be a service called by other application objects. A class diagram that exemplifies this approach is included below.

The following steps will be completed:

  1. MsSqlProviderLocator and WorkflowRuntimeLocator classes realization.
  2. Application build and run.
  3. Process execution.
keep in mind

Detailed information related to dependency injection services can be found in ASP.NET Core site.

Prerequisites​

  1. You should go through previous tutorials OR clone: States-and-activities branch.
  2. JetBrains Rider or Visual Studio.
  3. Command prompt or Terminal.

Backend​

The code improvements are done in the backed application, so we are going to describe them in this section.

WorkflowRuntimeLocator​

First, the Backend/WorkflowLib/WorkflowInit.cs must be deleted, and a new class WorkflowRuntimeLocator created. Here, all the initialization is defined as before, but a new constructor is added where the new provider MsSqlProviderLocator is passed on, as well as, the IWorkflowActionProvider, IWorkflowRuleProvider and IDesignerParameterFormatProvider that had been created previously. This class contains the initializations that are set up for the WorkflowRuntime.

Below the example code that you can use.

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.Plugins;

namespace WorkflowLib;

public class WorkflowRuntimeLocator
{
public WorkflowRuntime Runtime { get; private set; }

public WorkflowRuntimeLocator(MsSqlProviderLocator workflowProviderLocator,
IWorkflowActionProvider actionProvider, IWorkflowRuleProvider ruleProvider,
IDesignerParameterFormatProvider designerParameterFormatProvider)

{
// TODO If you have a license key, you have to register it here
// WorkflowRuntime.RegisterLicense(licenseKey);

var builder = new WorkflowBuilder<XElement>(
workflowProviderLocator.Provider,
new XmlWorkflowParser(),
workflowProviderLocator.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(workflowProviderLocator.Provider)
.EnableCodeActions()
.SwitchAutoUpdateSchemeBeforeGetAvailableCommandsOn()
// add custom activity
.WithCustomActivities(new List<ActivityBase> {new WeatherActivity()})
// add custom rule provider
.WithRuleProvider(ruleProvider)
.WithActionProvider(actionProvider)
.WithDesignerParameterFormatProvider(designerParameterFormatProvider)
.AsSingleServer();

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

Runtime = runtime;
}

}

MsSqlProviderLocator​

Next, a new class MsSqlProviderLocator is included which provides access to MS SQL database provider. The application configuration is defined by the constructor where we get ConnectionString from configRoot. Then, the provider is created and set to be invoked within properties.

using Microsoft.Extensions.Configuration;
using OptimaJet.Workflow.DbPersistence;

namespace WorkflowLib;

public class MsSqlProviderLocator
{
public MsSqlProviderLocator(IConfiguration configRoot)
{
Provider = new MSSQLProvider(configRoot.GetConnectionString("Default"));
}

public MSSQLProvider Provider { get; private set; }
}

Program​

Furthermore, some modifications should be included in Program. Here, all the required application objects i.e. classes must be registered as Singleton services. These services are created once and the same instance of the service is used for each request to such a service.

All the services associated to Workflow Engine have to be set as Singleton. Therefore, we register the new created MsSqlProviderLocator, besides IWorkflowActionProvider, IWorkflowRuleProvider, IDesignerParameterFormatProvider and WorkflowRuntimeLocator as Singleton services.

Then, the ASP.NET Core built mechanisms will substitute all the services through the constructors and assemble our application together.

info

Detailed information regarding service registration methods as Singleton can be found here.

using OptimaJet.Workflow.Core.Runtime;
using Microsoft.AspNetCore.SignalR;
using WorkflowApi;
using WorkflowLib;
using WorkflowApi.Hubs;

var builder = WebApplication.CreateBuilder(args);

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

...

// Objects registry
builder.Services.AddSingleton<MsSqlProviderLocator>();
builder.Services.AddSingleton<IWorkflowActionProvider, ActionProvider>();
builder.Services.AddSingleton<IWorkflowRuleProvider, SimpleRuleProvider>();
builder.Services.AddSingleton<IDesignerParameterFormatProvider, DesignerParameterFormatProvider>();
builder.Services.AddSingleton<WorkflowRuntimeLocator>();

var app = builder.Build();

var connectionString = app.Configuration.GetConnectionString("Default");
if (connectionString is null) throw new NullReferenceException("Default connection string is not set");
await WorkflowApi.DatabaseUpgrade.WaitForUpgrade(connectionString);

WorkflowLib.WorkflowInit.ConnectionString = connectionString;

// 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.MapHub<ProcessConsoleHub>("api/workflow/processConsole");

var processConsoleContext = app.Services.GetService<IHubContext<ProcessConsoleHub>>();

await app.Services.GetService<WorkflowRuntimeLocator>().Runtime.StartAsync();

app.Run();

ActionProvider​

Moreover, in ActionProvider has been already injected IHubContext<ProcessConsoleHub> on the constructor when SignalR was connected. There the _processConsoleHub is registered and passed on. It was done in previous tutorials, so we have not modified this class here.

using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using OptimaJet.Workflow.Core.Model;
using OptimaJet.Workflow.Core.Runtime;
using WorkflowApi.Hubs;

namespace WorkflowLib;

public class ActionProvider : IWorkflowActionProvider
{
private readonly Dictionary<string, Func<ProcessInstance, WorkflowRuntime, string, CancellationToken, Task>>
_asyncActions = new();

private IHubContext<ProcessConsoleHub> _processConsoleHub;

public ActionProvider(IHubContext<ProcessConsoleHub> processConsoleHub)
{
_processConsoleHub = processConsoleHub;
_asyncActions.Add(nameof(SendMessageToProcessConsoleAsync), SendMessageToProcessConsoleAsync);
}

...

//it is internal just to have possibility to use nameof()
internal async Task SendMessageToProcessConsoleAsync(ProcessInstance processInstance, WorkflowRuntime runtime,
string actionParameter, CancellationToken token)
{
await _processConsoleHub.Clients.All.SendAsync("ReceiveMessage", new
{
processId = processInstance.ProcessId,
message = actionParameter
}, cancellationToken: token);
}
}

DesignerController​

Some modifications related to the Controllers are required as well. Consequently, a new constructor is included in DesignerController where WorkflowRuntimeLocator is injected. Besides, the link to WorkflowRuntime constructor is collected. All methods where WorkflowInit.Runtime was stated are replaced by _runtime.

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

namespace WorkflowApi.Controllers;

public class DesignerController : Controller
{
// Adding constructor
private readonly WorkflowRuntime _runtime;

public DesignerController (WorkflowRuntimeLocator workflowRuntimeLocator)
{
_runtime = workflowRuntimeLocator.Runtime;
}
...

//Calling the Designer Api and store answer
var (result, hasError) = await WorkflowInit.Runtime.DesignerAPIAsync(parameters, filestream);
var (result, hasError) = await _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");

//response
return Content(result);
}
}

WorkflowController​

Finally, the WorkflowController is also modified. Likewise, a new constructor is included where MsSqlProviderLocator and WorkflowRuntimeLocator are injected. Accordingly, all methods where WorkflowInit.Provider is stated, must be replaced by _mssqlProvider, and also WorkflowInit.Runtime must be substituted by _runtime. It can be done through auto-replacement. And one more thing to do is to make the GetProcessInstanceAsync method non-static.

using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using OptimaJet.Workflow.Core.Entities;
using OptimaJet.Workflow.Core.Model;
using OptimaJet.Workflow.Core.Persistence;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.DbPersistence;
using WorkflowApi.Models;
using WorkflowLib;

namespace WorkflowApi.Controllers;

[Route("api/workflow")]
public class WorkflowController : ControllerBase
{
//adding constructor
private readonly MSSQLProvider _mssqlProvider;
private readonly WorkflowRuntime _runtime;

public WorkflowController(MsSqlProviderLocator msSqlProviderLocator,
WorkflowRuntimeLocator workflowRuntimeLocator)
{
_mssqlProvider = msSqlProviderLocator.Provider;
_runtime = workflowRuntimeLocator.Runtime;
}

/// <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();
await using var connection = _mssqlProvider.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
var list = await _mssqlProvider.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>
/// Creates a process instance for the process scheme
/// </summary>
/// <param name="schemeCode">Process scheme code</param>
/// <returns>Process instance</returns>
///[HttpGet]
[HttpPost]
[Route("createInstance/{schemeCode}")]
//public async Task<IActionResult> CreateInstance(string schemeCode)
public async Task<IActionResult> CreateInstance(string schemeCode, [FromBody] ProcessParametersDto dto)
{
// generating a new processId
var processId = Guid.NewGuid();

// creating a new process instance
var createInstanceParams = new CreateInstanceParams(schemeCode, processId);

if (dto.ProcessParameters.Count > 0)
{
var processScheme = await WorkflowInit.Runtime.Builder.GetProcessSchemeAsync(schemeCode);
var processScheme = await _runtime.Builder.GetProcessSchemeAsync(schemeCode);

foreach (var processParameter in dto.ProcessParameters)
{
var (name, value) =
GetParameterNameAndValue(processParameter.Name, processParameter.Value, processScheme);
if (processParameter.Persist)
{
createInstanceParams.AddPersistentParameter(name, value);
}
else
{
createInstanceParams.AddTemporaryParameter(name, value);
}
}
}

await _runtime.CreateInstanceAsync(createInstanceParams);

...

private async Task<ProcessInstance?> GetProcessInstanceAsync(Guid processId)
{
//it will be faster to use _runtime.Builder.GetProcessInstanceAsync call, because it doesn't load process parameters
var processInstance = await _runtime.Builder.GetProcessInstanceAsync(processId);

if (processInstance == null)
{
return null;
}

await _runtime.PersistenceProvider.FillSystemProcessParametersAsync(processInstance);
return processInstance;
}
}

Starting the application​

Once completing the code improvements, the application can be run, so start it by executing the following commands:

cd docker-files
docker compose up --build --force-recreate

Then, open the application URL http://localhost:3000/ in the browser.

Upload the process scheme​

The ParametersScheme.xml was modified to demonstrate the method's operation and the 'Change process state' feature in the previous tutorial States and Activities. The same process scheme will be used in this case.

Process Scheme

Download the updated ParametersScheme.xml here.

Click on tab Designer to upload the process scheme as indicated below:

Upload scheme

Then, save the scheme.

Running the process​

In this section, we are going to execute the support process and verify that everything works properly after implementing dependency injection.

Process execution​

First, we have to create a new process, so click on button Create process. You should indicate the issue Description as urgent support and fill out the Comment in the modal window 'Initial process parameters'. After that, click on blue button 'Create Process'.

Create process

A new message that indicates 'High priority processing' will be displayed by Process Console which means that actionProvider works correctly.

High priority processing

Next, we can select a Manager from First Line to check the command's availability i.e. we should verify that the ticket can be redirected, resolved or rejected.

Select an user

Click on command Redirect and fill out the Comment field in modal window 'Command parameters'. Then, click on blue button 'Execute: redirect'.

Command redirect

Once command Redirect is executed, you will see a new message by Process Console and the Division will be updated. It means that ruleProvider also works properly.

Issue is redirected

Finally, click on tab Designer to check designerParameterFormatProvider. You can open an Action in an Activity to see that form 'Edit parameter values' is displayed, and it can be edited.

Checking designer parameters

That's it! We have verified that main functionalities work rightly.

Conclusion​

We have demonstrated in this tutorial how can be implemented dependency injection pattern in our sample application for designing services that can be small, well-factored, and easily tested. Overall, the design in ASP.NET Core should avoid:

  • Stateful, static classes and members.
  • Creating global states.
  • Direct instantiation of dependent classes within services.