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"
}
]
}
Here are my results:
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, Func
2 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.
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.