Search code examples
elsa-workflows

Using a Elsa Workflow ForEach Loop Activity


I have my workflow triggered on a signal like so:

public async Task<IActionResult> StartApprovalProcess([FromBody] long requestId)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    // Get data object
    var payload = await _mainService.GetBudgetReleaseRequestPayload(requestId);

    var input = new Variables();

    input.SetVariable("Payload", payload);

    // Signal the workflow to start
    await _workflowInvoker.TriggerSignalAsync("StartApprovalPhase", input);

    return Ok("BRR registered");
}

Here is my Payload class:

public class BudgetReleaseRequestApprovalPhasePayloadModel
{
    public BudgetReleaseRequestApprovalPhasePayloadModel(BudgetReleaseRequestApprovalPhasePayloadDto model)
    {
        Id                 = model.Id;
        Description        = model.Description;
        Amount             = model.Amount;
        RequesterId        = model.RequesterId;
        SubmissionDate     = model.SubmissionDate;
        CostCenterName     = model.CostCenterName;
        ExpenseTypeName    = model.ExpenseTypeName;
        RequestTypeName    = model.RequestTypeName;
        AccountCode        = model.AccountCode;
        AccountName        = model.AccountName;
        BpsReferenceNumber = model.BpsReferenceNumber;

        ApproversList = new List<BudgetReleaseRequestApproverViewModel>();

        foreach (var budgetReleaseRequestApprover in model.ApproversList)
        {
            ApproversList.Add(new BudgetReleaseRequestApproverViewModel(budgetReleaseRequestApprover));
        }
    }

    public long     Id                 { get; set; }
    public string   Description        { get; set; }
    public decimal  Amount             { get; set; }
    public string   RequesterId        { get; set; }
    public DateTime SubmissionDate     { get; set; }
    public string   CostCenterName     { get; set; }
    public string   ExpenseTypeName    { get; set; }
    public string   RequestTypeName    { get; set; }
    public string   AccountCode        { get; set; }
    public string   AccountName        { get; set; }
    public string   BpsReferenceNumber { get; set; }

    public string AmountFormatted   => $"{Amount:N2} AED";
    public string DateFormatted     => $"{SubmissionDate:dd-MMM-yyyy}";
    public string CostCenterAndType => $"{CostCenterName}/{ExpenseTypeName}";
    public string AccountDetail     => $"{AccountCode} - {AccountName}";
    public int    ApproversCount    => ApproversList.Count;

    public IList<BudgetReleaseRequestApproverViewModel> ApproversList { get; set; }
}

And here is the class that acts as a collection:

public class BudgetReleaseRequestApproverViewModel
{
    public BudgetReleaseRequestApproverViewModel(BudgetReleaseRequestApprover model)
    {
        RequestId         = model.RequestId;
        RequestApproverId = model.RequestApproverId;
        ApproverId        = model.ApproverId;
        RequesterId       = model.RequesterId;
        ApproverSequence  = model.ApproverSequence;
        ActionId          = model.ActionId;
        RequestActionId   = model.RequestActionId;
    }

    public long   RequestId         { get; set; }
    public byte   RequestApproverId { get; set; }
    public string ApproverId        { get; set; }
    public string RequesterId       { get; set; }
    public byte   ApproverSequence  { get; set; }
    public Guid?  ActionId          { get; set; }
    public byte?  RequestActionId   { get; set; }

}

I followed the main guide (https://sipkeschoorstra.medium.com/building-workflow-driven-net-core-applications-with-elsa-139523aa4c50) and know that we need to implement a handler in order to have Liquid Expressions within workflow for both these models:

public class LiquidConfigurationHandler : INotificationHandler<EvaluatingLiquidExpression>
{
    public Task Handle(EvaluatingLiquidExpression notification, CancellationToken cancellationToken)
    {
        var context = notification.TemplateContext;
        context.MemberAccessStrategy.Register<BudgetReleaseRequestApprovalPhasePayloadModel>();
        context.MemberAccessStrategy.Register<BudgetReleaseRequestApproverViewModel>();

        return Task.CompletedTask;
    }
}

Here's my test workflow:

{
    "activities": [{
            "id": "abc63216-76e7-42b2-ab7b-5cdb6bbc3ed9",
            "type": "Signaled",
            "left": 122,
            "top": 365,
            "state": {
                "signal": {
                    "expression": "StartApprovalPhase",
                    "syntax": "Literal"
                },
                "name": "",
                "title": "Signal: Start Approval Phase",
                "description": "Trigger the workflow when this signal is received."
            },
            "blocking": false,
            "executed": false,
            "faulted": false
        }, {
            "id": "ac7669d6-b7e6-4139-825e-5f2b9c1dbdb8",
            "type": "SendEmail",
            "left": 553,
            "top": 379,
            "state": {
                "from": {
                    "expression": "[email protected]",
                    "syntax": "Literal"
                },
                "to": {
                    "expression": "[email protected]",
                    "syntax": "Literal"
                },
                "subject": {
                    "expression": "Workflow Testing",
                    "syntax": "Literal"
                },
                "body": {
                    "expression": "<p>BRR #{{ Input.Payload.Id }}</p>\r\n<p>Name: {{ Input.Payload.Description }}</p>\r\n<p>Amount: {{ Input.Payload.AmountFormatted }}</p>\r\n<p>Date: {{ Input.Payload.DateFormatted }}</p>\r\n<br />\r\n<p>Approvers: {{ Input.Payload.ApproversCount }}</p>",
                    "syntax": "Liquid"
                },
                "name": "",
                "title": "Email: Test",
                "description": ""
            },
            "blocking": false,
            "executed": false,
            "faulted": false
        }, {
            "id": "2efcffa9-8e18-45cf-aac8-fcfdc8846df8",
            "type": "ForEach",
            "left": 867,
            "top": 474,
            "state": {
                "collectionExpression": {
                    "expression": "{{ Input.Payload.ApproversList }}",
                    "syntax": "Liquid"
                },
                "iteratorName": "",
                "name": "",
                "title": "",
                "description": ""
            },
            "blocking": false,
            "executed": false,
            "faulted": false
        }, {
            "id": "7966b931-f683-4b81-aad4-ad0f6c628191",
            "type": "SendEmail",
            "left": 1042,
            "top": 675,
            "state": {
                "from": {
                    "expression": "[email protected]",
                    "syntax": "Literal"
                },
                "to": {
                    "expression": "[email protected]",
                    "syntax": "Literal"
                },
                "subject": {
                    "expression": "Looping #",
                    "syntax": "Literal"
                },
                "body": {
                    "expression": "Loop Details",
                    "syntax": "Literal"
                },
                "name": "",
                "title": "",
                "description": ""
            },
            "blocking": false,
            "executed": false,
            "faulted": false
        }, {
            "id": "5f246eda-271d-46ed-8efe-df0f26d542be",
            "type": "SendEmail",
            "left": 1163,
            "top": 325,
            "state": {
                "name": "",
                "from": {
                    "expression": "[email protected]",
                    "syntax": "Literal"
                },
                "to": {
                    "expression": "[email protected]",
                    "syntax": "Literal"
                },
                "subject": {
                    "expression": "Loop Over",
                    "syntax": "Literal"
                },
                "body": {
                    "expression": "Loop Finished",
                    "syntax": "Literal"
                },
                "title": "",
                "description": ""
            },
            "blocking": false,
            "executed": false,
            "faulted": false
        }
    ],
    "connections": [{
            "sourceActivityId": "2efcffa9-8e18-45cf-aac8-fcfdc8846df8",
            "destinationActivityId": "5f246eda-271d-46ed-8efe-df0f26d542be",
            "outcome": "Done"
        }, {
            "sourceActivityId": "abc63216-76e7-42b2-ab7b-5cdb6bbc3ed9",
            "destinationActivityId": "ac7669d6-b7e6-4139-825e-5f2b9c1dbdb8",
            "outcome": "Done"
        }, {
            "sourceActivityId": "ac7669d6-b7e6-4139-825e-5f2b9c1dbdb8",
            "destinationActivityId": "2efcffa9-8e18-45cf-aac8-fcfdc8846df8",
            "outcome": "Done"
        }, {
            "sourceActivityId": "2efcffa9-8e18-45cf-aac8-fcfdc8846df8",
            "destinationActivityId": "7966b931-f683-4b81-aad4-ad0f6c628191",
            "outcome": "Iterate"
        }, {
            "sourceActivityId": "7966b931-f683-4b81-aad4-ad0f6c628191",
            "destinationActivityId": "2efcffa9-8e18-45cf-aac8-fcfdc8846df8",
            "outcome": "Done"
        }
    ]
}

Visual: Elsa-workflow

Here are my results:

  • Signal: Works
  • First Email: Works:

Email-Sample

  • ForEach Fails and I catch this in debug:

    fail: Elsa.Expressions.WorkflowExpressionEvaluator[0] Error while evaluating JavaScript expression "{{ Input.Payload.ApproversList }}". Message: Input is not defined ReferenceError: Input is not defined fail: Elsa.Services.ActivityInvoker[0] Error while invoking activity 2efcffa9-8e18-45cf-aac8-fcfdc8846df8 of workflow de8e12d4645e4480abccbbe562b48448 Elsa.Exceptions.WorkflowException: Error while evaluating JavaScript expression "{{ Input.Payload.ApproversList }}". Message: Input is not defined ---> ReferenceError: Input is not defined --- End of inner exception stack trace --- at Elsa.Expressions.WorkflowExpressionEvaluator.EvaluateAsync(IWorkflowExpression expression, Type type, WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) at Elsa.Extensions.WorkflowExpressionEvaluatorExtensions.EvaluateAsync[T](IWorkflowExpressionEvaluator evaluator, IWorkflowExpression1 expression, WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) at Elsa.Activities.ControlFlow.Activities.ForEach.OnExecuteAsync(WorkflowExecutionContext context, CancellationToken cancellationToken) at Elsa.Services.ActivityInvoker.InvokeAsync(WorkflowExecutionContext workflowContext, IActivity activity, Func2 invokeAction)

    fail: Elsa.Services.WorkflowInvoker[0] IWorkflowEventHandler thrown from Elsa.WorkflowEventHandlers.PersistenceWorkflowEventHandler by DbUpdateException Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Engine' with type 'Jint.Engine'. Path 'Exception.InnerException.Error.Engine.Global'.

I need to iterate a BudgetReleaseRequestApproverViewModel, send email, wait action, repeat but I cannot figure out the Loop.


Solution

  • This answer is based on my comments provided on the GitHub issue which is a repeat of the OP's question. I'm providing the below for completeness' sake.

    Try using the input function for the ForEach activity (make sure that the selected syntax is JavaScript):

    input('PayLoad').ApproverList

    This will get the input named "PayLoad".

    I don't know if Liquid is supposed to work. From a UX point of view, we should either make sure it does, or not even allow that option if it doesn't.