Using a Elsa Workflow ForEach Loop Activity - elsa-workflows

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": "my.email#acme.co",
"syntax": "Literal"
},
"to": {
"expression": "my.email#acme.co",
"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": "my.email#acme.co",
"syntax": "Literal"
},
"to": {
"expression": "my.email#acme.co",
"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": "my.email#acme.co",
"syntax": "Literal"
},
"to": {
"expression": "my.email#acme.co",
"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:
Here are my results:
Signal: Works
First Email: Works:
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.

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.

Related

Filter IEnumerable Data using Linq on ASP.NET Web API 2

I need to filter my data.but i don't know how to use Linq.
The ClassRoom Model
public class ClassRoom
{
public int RoomID { get; set; }
public string Name { get; set; }
public List<Student> Students { get; set; }
}
The Student Model
public class Student
{
public int StudentId { get; set; }
public string Name { get; set; }
public int StyleId {get;set;}
}
Example JSON DATA
var json=
[
{
"RoomID": 1,
"Name": "A Class",
"Students": [{
"StudentId": 1,
"Name":"Charlie",
"StyleId":"1"
},
{
"StudentId": 2,
"Name":"Tom",
"StyleId":"2"
}
]
},
{
"RoomID": 2,
"Name": "B Class",
"Students": [{
"StudentId": 3,
"Name":"ALLEN",
"StyleId":"2"
},
{
"StudentId": 4,
"Name":"Jeremy",
"StyleId":"2"
},
{
"StudentId": 5,
"Name":"Curry",
"StyleId":"3"
}
]
}
]
If i want get StyleID equals "2", and below is expected a answer.
var json =
[
{
"RoomID": 1,
"Name": "A Class",
"Students": [
{
"StudentId": 2,
"Name":"Tom",
"StyleId":"2"
}
]
},
{
"RoomID": 2,
"Name": "B Class",
"Students": [{
"StudentId": 3,
"Name":"ALLEN",
"StyleId":"2"
},
{
"StudentId": 4,
"Name":"Jeremy",
"StyleId":"2"
}
]
}
]
Here is how i filer, but not correct. how can i get expected data?
json.Select( v => v.Students.where( i => i.StyleId == "2") )
thanks.
Try linq query as below.
var result = json.Where(v => v.Students.Any(y => y.StyleId == "2"))
.Select(v => new ClassRoom() {
RoomID = v.RoomID,
Name = v.Name,
Students = v.Students.Where(y => y.StyleId == "2")
});

Type another answer in JSON enum FormFlow dialog

I am using JSON FormFlow dialogs.
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var activity = await argument;
if (activity.Type == ActivityTypes.Message)
{
context.Call(FormDialog.FromForm(() => BuildJsonForm(), FormOptions.PromptInStart), DialogCompleted);
}
}
public static IForm<JObject> BuildJsonForm()
{
using (var stream = File.OpenRead("my.json"))
{
var schema = JObject.Parse(new StreamReader(stream).ReadToEnd());
return new FormBuilderJson(schema)
.AddRemainingFields()
.Build();
}
}
Inside of my.json I have enum:
"properties": {
"EnumExample": {
"Prompt": { "Patterns": [ "To what language translate {||}" ] },
"Describe": { "Image": "" },
"type": [
"string",
"null"
],
"enum": [
"en",
"fr",
"ru",
"de",
"es"
],
"Values": {
"en": {
"Describe": "English"
},
"fr": {
"Describe": "French"
},
"ru": {
"Describe": "Russian"
},
"de": {
"Describe": "German"
},
"es": {
"Describe": "Spanish"
}
}
}
The question : how I can support selecting by user one of Enum options and also free text typing , so I can run another dialog or run Luis API on that input. For example user will type " I want to talk with operator" instead of answering Enum in my JSON FormFlow dialog?
Thanks

ASP.NET Core Web API Swagger docs incorrect with reference loop between classes

I have the following classes in my application:
public class Person
{
public int Id { get; set; }
public Guid PublicKey { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public DateTime Date { get; set; }
public virtual Person Person { get; set; }
public int PersonId { get; set; }
}
and the following API controller that just gets a list of all the people (there are always 2, and they always have 3 orders each):
[Route("api/[controller]")]
public class PeopleController : Controller
{
// GET api/people
[HttpGet]
public List<Person> Get()
{
PeopleService people = new PeopleService();
return people.GetAllPeople();
}
}
I should point out that I had issues from the start with the navigation property on the Order class referring back to the Person that owns it, as the Json.NET formatter doesn't like this out of the box and you have to configure it to ignore reference loops. I have done this in the Startup.cs like so:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(o =>
{
o.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
o.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
services.AddSwaggerGen();
}
This works great, and when I make a request I get the following response:
[
{
"orders": [
{
"id": 1,
"date": "2016-10-26T17:16:35.21",
"personId": 1
},
{
"id": 2,
"date": "2016-10-26T17:16:35.21",
"personId": 1
}
],
"id": 1,
"publicKey": "b6a7c21c-86d8-4bb9-9a05-bd394e6ed0c9",
"firstName": "Lauren",
"lastName": "Phillips"
},
{
"orders": [
{
"id": 3,
"date": "2016-10-26T17:16:35.21",
"personId": 2
},
{
"id": 4,
"date": "2016-10-26T17:16:35.21",
"personId": 2
}
],
"id": 2,
"publicKey": "8b5a90b4-a9a2-4a0e-96dd-529962972456",
"firstName": "Robert",
"lastName": "West"
}
]
I am using Swashbuckle to generate Swagger docs for my API. The "Example Value" that is generated by Swashbuckle/Swagger seems to be including the Person again within each order:
[
{
"id": 0,
"publicKey": "string",
"firstName": "string",
"lastName": "string",
"orders": [
{
"id": 0,
"date": "2016-10-27T14:19:52.437Z",
"person": {
"id": 0,
"publicKey": "string",
"firstName": "string",
"lastName": "string",
"orders": [
{}
]
},
"personId": 0
}
]
}
]
I don't want people consuming the API to expect the person to be included again in each order, especially as that isn't what you actually get because I configured it to ignore reference loops above. I imagine that this issue is related to the reference loop, but I am not sure. Does anyone know how to fix this?
Probably, you can use the attribute - [JsonIgnore] to not show that up in the swagger documentation

Search by first item of nest field List<object> in elasticsearch

How can i search by first item of nest field type List in elasticsearch?
public class Data
{
public int Id{get;set;}
public string Name{get;set;}
public List<History> Logs{get;set;}
}
public class History
{
public byte Status { get; set; }
public string Note { get; set; }
public DateTime DateProcessed { get; set; }
}
Example, i have 3 records like as:
PUT /my_index/Data/1
{
"name": "name 1",
"logs": [
{
"status": "2",
"note": "processed",
"dateProcessed": "2016-04-11"
},
{
"status": "1",
"note": "new",
"dateProcessed": "2016-04-10"
}
]
}
PUT /my_index/Data/2
{
"name": "name 2",
"logs": [
{
"status": "1",
"note": "new",
"dateProcessed": "2016-04-11"
}
]
}
PUT /my_index/Data/3
{
"name": "name 2",
"logs": [
{
"status": "3",
"note": "error",
"dateProcessed": "2016-04-11"
},
{
"status": "1",
"note": "new",
"dateProcessed": "2016-04-10"
}
]
}
Now, i want to get records which have first item of list (field logs) with status equal 1.
In above example, result is second record.
Please help me, thank you!
You can you Script Query, but that would be very slow.
{
"filter": {
"script": {
"script": "_source.logs?.first()?.status.equals('1')"
}
}
}
Make sure to enable scripting
The way you should go seems like Nested Query.
You should define your list as Nested during mapping. Then you can write Nested Queries on them and filter your "Data" depending on inner hits of "History".

WebAPI 2 Odata Filter not working

I have a problem with using multiple filters on my WebAPI 2 Odata project.
We only want JSON output and only have one object type to query so we set the url to "/" without the possibility to use a different controller.
I have an object I want to query with the following properties:
public class Content
{
public int Id { get; set; }
public string Title { get; set; }
public string Excerpt { get; set; }
public string Link { get; set; }
public IList<Tag> Tags { get; set; }
}
public class Tag
{
public string Id { get; set; }
public string Name { get; set; }
}
And the controller code looks like this:
public class ContentController : ApiController
{
private readonly IContentRepository _repository;
// constructor
public ContentController(IContentRepository repository)
{
_repository = repository;
}
[Queryable]
public IQueryable<Content> Index()
{
// IContentRepository.GetAll returns an IEnumerable List of Content
return _repository.GetAll().AsQueryable();
}
}
Now, I've mocked some testdata with adding multiple objects to the repository that have multiple tags with values set to either (test1, test2 or test3). Now when i Query
http://localhost:xxx?$filter=Tags/any(o: o/Id eq 'test1')
I get all objects with Tag/Id set to 'test1'. But if I query
http://localhost:xxx?$filter=Tags/any(o: o/Id eq 'test1' and o/Id eq 'test2')
I get no result (JSON return = []). But it should return objects that have both tags.
What am I doing wrong?
EDIT:
My sample data JSON looks like this:
[
{
"Id": 1,
"Title": "TESTOBJECT 1",
"Excerpt": "",
"Link": "",
"Tags": [
{
"Id": "test1",
"Name": "Test Tag 1",
}
],
},
{
"Id": 2,
"Title": "TESTOBJECT 2",
"Excerpt": "",
"Link": "",
"Tags": [
{
"Id": "test2",
"Name": "Test Tag 2",
}
],
},
{
"Id": 3,
"Title": "TESTOBJECT 3",
"Excerpt": "",
"Link": "",
"Tags": [
{
"Id": "test3",
"Name": "Test Tag 3",
}
],
},
{
"Id": 4,
"Title": "TESTOBJECT 4",
"Excerpt": "",
"Link": "",
"Tags": [
{
"Id": "test1",
"Name": "Test Tag 1",
},
{
"Id": "test2",
"Name": "Test Tag 2",
}
],
},
{
"Id": 5,
"Title": "TESTOBJECT 5",
"Excerpt": "",
"Link": "",
"Tags": [
{
"Id": "test1",
"Name": "Test Tag 1",
}
],
}
]
Now query one gives me object 1,4,5 and I would expect query two to give me object 4. How can I accomplish this with odata?
Did you want this?
http://localhost:xxx?$filter=Tags/any(o: o/Id eq 'test1') and Tags/any(o: o/Id eq 'test2')
((X == "test1") AND (X == "test2")) will always return false!
If you want to identify Tags whose property Id is test1 or test2, use the or operator.

Resources