From f4f2f01a4950dbff0ba79a06802ae5f344441603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Poul=20Kjeldager=20S=C3=B8rensen?= Date: Thu, 17 Feb 2022 04:29:56 +0100 Subject: [PATCH 1/4] added scope support --- apps/WorkflowEngine.DemoApp/Startup.cs | 201 ++++++++++++++++- src/WorkflowEngine.Core/Action.cs | 5 + src/WorkflowEngine.Core/ActionExecutor.cs | 80 ++++++- src/WorkflowEngine.Core/ActionMetadata.cs | 49 ++++- .../DefaultOutputsRepository.cs | 202 ++++++++++++++++++ .../ExpressionEngineExtensions.cs | 55 +++++ .../Expressions/ItemsFunction.cs | 33 +++ .../Expressions/OutputsFunction.cs | 37 ++++ .../Expressions/TriggerBodyFunction.cs | 28 +++ .../Expressions/TriggerOutputsFunction.cs | 36 ++++ src/WorkflowEngine.Core/IAction.cs | 7 +- src/WorkflowEngine.Core/IActionExecutor.cs | 2 +- .../IActionImplementation.cs | 2 +- .../IActionImplementationExtenssions.cs | 4 +- src/WorkflowEngine.Core/IActionResult.cs | 4 +- src/WorkflowEngine.Core/IOutputsRepository.cs | 21 ++ src/WorkflowEngine.Core/ITrigger.cs | 14 +- src/WorkflowEngine.Core/ITriggerContext.cs | 9 +- src/WorkflowEngine.Core/IWorkflowExecutor.cs | 9 +- src/WorkflowEngine.Core/TriggerContext.cs | 12 +- src/WorkflowEngine.Core/TriggerMetadata.cs | 2 +- src/WorkflowEngine.Core/WorkflowActions.cs | 59 +++++ .../WorkflowEngine.Core.csproj | 15 +- src/WorkflowEngine.Core/WorkflowExecutor.cs | 32 ++- src/WorkflowEngine.Hangfire/Class1.cs | 62 ------ src/WorkflowEngine.Hangfire/ForloopAction.cs | 65 ++++++ .../HangfireWorkflowExecutor.cs | 92 ++++++++ .../IHangfireActionExecutor.cs | 5 +- .../IHangfireWorkflowExecutor.cs | 2 +- 29 files changed, 1033 insertions(+), 111 deletions(-) create mode 100644 src/WorkflowEngine.Core/DefaultOutputsRepository.cs create mode 100644 src/WorkflowEngine.Core/ExpressionEngineExtensions.cs create mode 100644 src/WorkflowEngine.Core/Expressions/ItemsFunction.cs create mode 100644 src/WorkflowEngine.Core/Expressions/OutputsFunction.cs create mode 100644 src/WorkflowEngine.Core/Expressions/TriggerBodyFunction.cs create mode 100644 src/WorkflowEngine.Core/Expressions/TriggerOutputsFunction.cs create mode 100644 src/WorkflowEngine.Core/IOutputsRepository.cs delete mode 100644 src/WorkflowEngine.Hangfire/Class1.cs create mode 100644 src/WorkflowEngine.Hangfire/ForloopAction.cs create mode 100644 src/WorkflowEngine.Hangfire/HangfireWorkflowExecutor.cs diff --git a/apps/WorkflowEngine.DemoApp/Startup.cs b/apps/WorkflowEngine.DemoApp/Startup.cs index f030020..7476a61 100644 --- a/apps/WorkflowEngine.DemoApp/Startup.cs +++ b/apps/WorkflowEngine.DemoApp/Startup.cs @@ -1,18 +1,24 @@ +using ExpressionEngine; using Hangfire; using Hangfire.SqlServer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using WorkflowEngine.Core; +using WorkflowEngine.Core.Actions; +using WorkflowEngine.Core.Expressions; namespace WorkflowEngine.DemoApp { @@ -23,7 +29,7 @@ public class SendEmailAction : IActionImplementation public const string SendEmailActionType = "SendEmailAction"; - public async ValueTask ExecuteAsync(IWorkflow workflow, IAction action) + public async ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) { await Task.Delay(TimeSpan.FromMinutes(3)); @@ -36,7 +42,7 @@ public class EAVCreateAction : IActionImplementation public const string EAVCreateActionType = "EAVCreateAction"; - public async ValueTask ExecuteAsync(IWorkflow workflow, IAction action) + public async ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) { Console.WriteLine($"Hello world from 1"); await Task.Delay(TimeSpan.FromMinutes(3)); @@ -46,9 +52,60 @@ public async ValueTask ExecuteAsync(IWorkflow workflow, IAction action) return null; } } + + public class RetrieveRecordAction : IActionImplementation + { + private readonly IExpressionEngine expressionEngine; + + public RetrieveRecordAction(IExpressionEngine expressionEngine) + { + this.expressionEngine=expressionEngine; + } + public ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) + { + // var next = workflow.Manifest.Actions.FindAction(action.Key); + var inputs = action.Inputs; + + return new ValueTask(new + { + id = Guid.Parse(inputs["recordId"]?.ToString()), + targetgroupid = Guid.NewGuid() + }); + } + } + public class RetrieveTargetGroupTargetsAction : IActionImplementation + { + public ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) + { + return new ValueTask(new + { + value = new[] { new { targetid=Guid.NewGuid()}, new { targetid = Guid.NewGuid() } } + }); + } + } + public class FindFormSubmissionForAccountAction : IActionImplementation + { + public ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) + { + var next = workflow.Manifest.Actions.FindAction(action.Key); + + return new ValueTask(new { formsubmissionid = Guid.NewGuid() }); + } + } + public class CreateESDHCaseFromFormSubmissionAction : IActionImplementation + { + public ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) + { + var next = workflow.Manifest.Actions.FindAction(action.Key); + + return new ValueTask(new { id = Guid.NewGuid() }); + } + } + + public class Startup { @@ -73,6 +130,8 @@ public void ConfigureServices(IServiceCollection services) services.AddHangfireServer(); services.AddRazorPages(); + services.AddExpressionEngine(); + services.AddTransient(); services.AddTransient(); @@ -81,7 +140,16 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddAction(SendEmailAction.SendEmailActionType); services.AddAction(EAVCreateAction.EAVCreateActionType); - + services.AddAction(); + services.AddAction(); + services.AddAction(); + services.AddAction(); + services.AddAction("Foreach"); + services.AddSingleton(); + services.AddFunctions(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(new Workflow { Id = Guid.Empty, @@ -93,11 +161,11 @@ public void ConfigureServices(IServiceCollection services) ["Trigger"] = new TriggerMetadata { Type = "EAVTrigger", - Inputs = - { - ["operation"] = "create", - ["entity"] ="targetgroupresults" - } + //Inputs = + //{ + // ["operation"] = "create", + // ["entity"] ="targetgroupresults" + //} } }, Actions = @@ -123,7 +191,87 @@ public void ConfigureServices(IServiceCollection services) } }); - // services.AddScoped() + services.AddSingleton(new Workflow + { + Id = Guid.Parse("dd087e82-c61a-4ce2-b961-b976baa4bf17"), + Version = "1.0", + Manifest = new WorkflowManifest + { + Triggers = + { + ["Trigger"] = new TriggerMetadata + { + Type = "Manual", + + } + }, + Actions = + { + [nameof(RetrieveRecordAction)] = new ActionMetadata + { + Type = nameof(RetrieveRecordAction), + Inputs = + { + ["entityName"] = "forms", + ["recordId"] = "@triggerBody()?['recordId']" + } + }, + [nameof(RetrieveTargetGroupTargetsAction)] = new ActionMetadata + { + Type = nameof(RetrieveTargetGroupTargetsAction), + RunAfter = new WorkflowRunAfterActions + { + [nameof(RetrieveRecordAction)] = new []{"Succeded"} + }, + Inputs = + { + ["targetgroupid"] = $"@outputs('{nameof(RetrieveRecordAction)}')?['body/targetgroupid']" + } + }, + ["Loop_over_targetgroup"] = new ForLoopActionMetadata + { + RunAfter = new WorkflowRunAfterActions + { + [nameof(RetrieveTargetGroupTargetsAction)] = new []{"Succeded"} + }, + ForEach = $"@outputs('{nameof(RetrieveTargetGroupTargetsAction)}')?['body/value']", + Type = "Foreach", + Inputs = + { + + }, + Actions = + { + [nameof(FindFormSubmissionForAccountAction)] = new ActionMetadata + { + Type = nameof(FindFormSubmissionForAccountAction), + Inputs = + { + ["accountid"] = "@items('Loop_over_targetgroup')?['targetid']", + ["formid"] = "@triggerBody()?['recordId']", + } + }, + [nameof(CreateESDHCaseFromFormSubmissionAction)] = new ActionMetadata + { + RunAfter = new WorkflowRunAfterActions + { + [nameof(FindFormSubmissionForAccountAction)] = new []{"Succeded"} + }, + Type = nameof(CreateESDHCaseFromFormSubmissionAction), + Inputs = + { + ["formsubmissionid"] = $"@outputs('{nameof(FindFormSubmissionForAccountAction)}')?['body/formsubmissionid']", + } + } + + } + } + + } + } + }); + + // services.AddScoped() } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -148,7 +296,40 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IService var workflows = await c.RequestServices.GetRequiredService().GetAllWorkflows(); var a = Hangfire.BackgroundJob.Enqueue( - (executor) => executor.TriggerAsync(new TriggerContext { Workflow = workflows.First() })); + (executor) => executor.TriggerAsync(new TriggerContext { + Workflow = workflows.First(), + Trigger = new Trigger { ScheduledTime = DateTimeOffset.UtcNow, Key = workflows.First().Manifest.Triggers.First().Key, + Type =workflows.First().Manifest.Triggers.First().Value.Type }, RunId = Guid.NewGuid() })); + + await c.Response.WriteAsync("Background JOb:" + a); + + // c.RequestServices.GetRequiredService + + + }); + + endpoints.MapGet("/runs", async c => + { + await c.Response.WriteAsync(JToken.FromObject(c.RequestServices.GetRequiredService()).ToString()); + }); + endpoints.MapGet("/magic/{id}", async c => + { + var workflows = await c.RequestServices.GetRequiredService().GetAllWorkflows(); + var inputs = new Dictionary + { + ["recordId"] = Guid.NewGuid(), + ["entityName"] ="Forms", + }; + var a = Hangfire.BackgroundJob.Enqueue( + (executor) => executor.TriggerAsync(new TriggerContext { + RunId = Guid.NewGuid() , + Trigger = new Trigger { + Inputs = inputs, + ScheduledTime = DateTimeOffset.UtcNow, Type=workflows.First(w => w.Id.ToString() == c.GetRouteValue("id") as string).Manifest.Triggers.FirstOrDefault().Value.Type, + Key = workflows.First(w => w.Id.ToString() == c.GetRouteValue("id") as string).Manifest.Triggers.FirstOrDefault().Key + }, + Workflow = workflows.First(w=>w.Id.ToString() == c.GetRouteValue("id") as string) + })); await c.Response.WriteAsync("Background JOb:" + a); diff --git a/src/WorkflowEngine.Core/Action.cs b/src/WorkflowEngine.Core/Action.cs index 90ddba0..bd6dd38 100644 --- a/src/WorkflowEngine.Core/Action.cs +++ b/src/WorkflowEngine.Core/Action.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace WorkflowEngine.Core { @@ -8,6 +9,10 @@ public class Action : IAction, IFormattable public string Type { get; set; } public string Key { get; set; } + public Guid RunId { get; set; } + public IDictionary Inputs { get; set; } + public int Index { get; set; } + public bool ScopeMoveNext { get; set; } public string ToString(string format, IFormatProvider formatProvider) { diff --git a/src/WorkflowEngine.Core/ActionExecutor.cs b/src/WorkflowEngine.Core/ActionExecutor.cs index 74d8cc5..9baae9c 100644 --- a/src/WorkflowEngine.Core/ActionExecutor.cs +++ b/src/WorkflowEngine.Core/ActionExecutor.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +using ExpressionEngine; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -6,32 +8,98 @@ namespace WorkflowEngine.Core { + public interface IScopeContext + { + public string Scope { get; set; } + } + public class ScopeContext : IScopeContext + { + public string Scope { get; set; } + } public class ActionExecutor : IActionExecutor { + private readonly IOutputsRepository outputsRepository; private readonly IServiceProvider serviceProvider; + private readonly ILogger logger; + private readonly IScopeContext scopeContext; + private readonly IExpressionEngine expressionEngine; private Dictionary _implementations; - public ActionExecutor(IEnumerable implementations, IServiceProvider serviceProvider) + public ActionExecutor( + IEnumerable implementations, + IOutputsRepository outputsRepository, + IServiceProvider serviceProvider, + ILogger logger, + IScopeContext scopeContext, + IExpressionEngine expressionEngine) { _implementations = implementations?.ToDictionary(k => k.Type) ?? throw new ArgumentNullException(nameof(implementations)); + this.outputsRepository=outputsRepository??throw new ArgumentNullException(nameof(outputsRepository)); this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.logger=logger??throw new ArgumentNullException(nameof(logger)); + this.scopeContext=scopeContext; + this.expressionEngine=expressionEngine??throw new ArgumentNullException(nameof(expressionEngine)); } - public async ValueTask ExecuteAsync(IWorkflow workflow, IAction action) + public async ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) { try { - var actionMetadata = workflow.Manifest.Actions.Single(k => k.Key == action.Key).Value; + if (action.ScopeMoveNext) + { + await outputsRepository.EndScope(context, workflow, action); + } + + var actionMetadata = workflow.Manifest.Actions.FindAction(action.Key); + scopeContext.Scope=action.Key; + action.Inputs = await expressionEngine.ResolveInputs(actionMetadata,logger); + + { + //if (workflow.Manifest.Actions.FindParentAction(action.Key) is ForLoopActionMetadata parent) + //{ + // await outputsRepository.AddArrayInput(context, workflow, action); + //} + //else + //if (actionMetadata is ForLoopActionMetadata) + //{ + // await outputsRepository.StartScope(context, workflow, action); + //} + //else + { + await outputsRepository.AddInput(context, workflow, action); + } + } + var actionImplementation = serviceProvider.GetRequiredService(_implementations[actionMetadata.Type].Implementation) as IActionImplementation; - return new ActionResult { + + + var result = new ActionResult { Key = action.Key, Status = "Succeded", - Result = await actionImplementation.ExecuteAsync(workflow,action) + Result = await actionImplementation.ExecuteAsync(context,workflow, action) }; + + { + //if (workflow.Manifest.Actions.FindParentAction(action.Key) is ForLoopActionMetadata parent) + //{ + // await outputsRepository.AddArrayItemAsync(context, workflow, action.Key, result); + //} + //else + //if (actionMetadata is ForLoopActionMetadata) + //{ + // await outputsRepository.AddScopeItem(context, workflow, action, result); + //} + //else + { + await outputsRepository.AddAsync(context, workflow, action, result); + } + } + + return result; }catch(Exception ex) diff --git a/src/WorkflowEngine.Core/ActionMetadata.cs b/src/WorkflowEngine.Core/ActionMetadata.cs index 55bc3d3..5bdc56d 100644 --- a/src/WorkflowEngine.Core/ActionMetadata.cs +++ b/src/WorkflowEngine.Core/ActionMetadata.cs @@ -1,17 +1,64 @@ -using System.Collections; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; namespace WorkflowEngine.Core { /// /// Represent the metadata for a action /// + [JsonConverter(typeof(ActionMetadataJsonConvert))] public class ActionMetadata { public WorkflowRunAfterActions RunAfter { get; set; } = new WorkflowRunAfterActions(); public string Type { get; set; } public IDictionary Inputs { get; set; } = new Dictionary(); + public bool ShouldRun(string key, string status) + { + if (key.Contains(".")) + { + key = key.Substring(key.LastIndexOf(".")+1); + } + + return RunAfter[key].Contains(status); + } + } + + public class ActionMetadataJsonConvert : JsonConverter + { + public override bool CanRead => true; + override public bool CanWrite => false; + + public override bool CanConvert(Type objectType) + { + return typeof(ActionMetadata).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + + var jtoken = JToken.ReadFrom(reader) as JObject; + var type = jtoken.GetValue("type", StringComparison.OrdinalIgnoreCase).Value(); + + if (type=="Foreach") + { + var a = new ForLoopActionMetadata(); + serializer.Populate(jtoken.CreateReader(), a); + return a; + } + var val = new ActionMetadata(); + serializer.Populate(jtoken.CreateReader(), val); + return val; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } } } diff --git a/src/WorkflowEngine.Core/DefaultOutputsRepository.cs b/src/WorkflowEngine.Core/DefaultOutputsRepository.cs new file mode 100644 index 0000000..23176bb --- /dev/null +++ b/src/WorkflowEngine.Core/DefaultOutputsRepository.cs @@ -0,0 +1,202 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core +{ + public class DefaultOutputsRepository : IOutputsRepository + { + public ConcurrentDictionary Runs { get; set; } = new ConcurrentDictionary(); + + public ValueTask AddScopeItem(IRunContext context, IWorkflow workflow, IAction action, IActionResult result) + { + return AddAsync(context,workflow,action,result); + JToken run = GetOrCreateRun(context); + + var obj = GetOrCreateStateObject(action.Key, run); + + obj.Merge(JToken.FromObject(new { status = result.Status, body = result.Result, failedReason = result.FailedReason })); + + return new ValueTask(); + } + public ValueTask AddAsync(IRunContext context, IWorkflow workflow, IAction action, IActionResult result) + { + JToken run = GetOrCreateRun(context); + + var obj = GetOrCreateStateObject(action.Key, run); + + obj.Merge(JToken.FromObject(new { status = result.Status, body = result.Result, failedReason = result.FailedReason })); + + + + return new ValueTask(); + } + + private static JObject GetOrCreateStateObject(string key, JToken run) + { + JToken actions =run["actions"]; + + var idx = key.IndexOf('.'); + while (idx != -1) + { + actions = actions[key.Substring(0, idx)]; + key = key.Substring(idx + 1); + idx = key.IndexOf('.'); + + + actions = actions["actions"] ?? (actions["actions"] = new JObject()); + } + + JObject obj = actions[key] as JObject; + if(obj == null) + { + actions[key]=obj=new JObject(); + } + + return obj; + } + + private JToken GetOrCreateRun(IRunContext context) + { + return Runs.GetOrAdd(context.RunId, (id) => new JObject(new JProperty("actions", new JObject()), new JProperty("triggers", new JObject()))); + } + + public ValueTask AddAsync(IRunContext context, IWorkflow workflow, ITrigger trigger) + { + JToken run = GetOrCreateRun(context); + + run["triggers"][trigger.Key] = JToken.FromObject( new { time=trigger.ScheduledTime, body = trigger.Inputs }); + + return new ValueTask(); + } + + + public ValueTask GetTriggerData(Guid id) + { + var run = Runs[id]; + return new ValueTask(run["triggers"].OfType().FirstOrDefault().Value); + } + + public ValueTask GetOutputData(Guid id, string v) + { + var run = Runs[id]; + + var obj = GetOrCreateStateObject(v, run); + + return new ValueTask(obj); + } + + public ValueTask AddArrayItemAsync(IRunContext context, IWorkflow workflow, string key, IActionResult result) + { + + JToken run = GetOrCreateRun(context); + + + var obj1 = GetOrCreateStateObject(key, run); + + obj1.Merge(JToken.FromObject(new { status = result.Status, body = result.Result, failedReason = result.FailedReason })); + + + // var obj = GetOrCreateStateObject(key.Substring(0, key.LastIndexOf('.')), run); + + // var body = obj["items"] as JArray; + // if (body==null) + // obj["items"] =body= new JArray(); + + // var lastItem = body[body.Count-1] as JObject; + // var actions = lastItem["actions"] as JObject; + + // var itteration = actions[key.Substring(key.LastIndexOf('.')+1)] as JObject; + // // if (itteration==null) + //// actions[key.Substring(key.LastIndexOf('.')+1)] = itteration = new JObject(); + + // itteration.Merge(JToken.FromObject(JToken.FromObject(new { status = result.Status, body = result.Result, failedReason = result.FailedReason }))); + + + + + return new ValueTask(); + } + + public ValueTask AddArrayInput(IRunContext context, IWorkflow workflow, IAction action) + { + + + JToken run = GetOrCreateRun(context); + + var obj = GetOrCreateStateObject(action.Key, run); + + obj.Merge(JToken.FromObject(new { input = action.Inputs })); + + + //var obj = GetOrCreateStateObject(action.Key.Substring(0, action.Key.LastIndexOf('.')), run); + + + //var body = obj["items"] as JArray; + + //var lastItem = body[body.Count-1] as JObject; + //var actions = lastItem["actions"] as JObject; + + //actions[action.Key.Substring(action.Key.LastIndexOf('.')+1)] = JToken.FromObject(new { input = action.Inputs }); + + + return new ValueTask(); + + + } + public ValueTask EndScope(IRunContext context, IWorkflow workflow, IAction action) + { + JToken run = GetOrCreateRun(context); + + var obj = GetOrCreateStateObject(action.Key, run); + + var body = obj["items"] as JArray; + if (body==null) + obj["items"] =body= new JArray(); + + var actions = obj["actions"]; + actions.Parent.Remove(); + body.Add(actions); + + + action.Index= body.Count; + + return new ValueTask(); + + } + public ValueTask StartScope(IRunContext context, IWorkflow workflow, IAction action) + { + JToken run = GetOrCreateRun(context); + + var obj = GetOrCreateStateObject(action.Key, run); + + var body = obj["items"] as JArray; + if (body==null) + obj["items"] =body= new JArray(); + + var lastItem = JToken.FromObject(new { actions = new JObject() }); + + body.Add(lastItem); + + return new ValueTask(); + } + + public ValueTask AddInput(IRunContext context, IWorkflow workflow, IAction action) + { + JToken run = GetOrCreateRun(context); + + var obj = GetOrCreateStateObject(action.Key, run); + + obj.Merge(JToken.FromObject(new { input = action.Inputs })); + + return new ValueTask(); + } + + + + } + + +} diff --git a/src/WorkflowEngine.Core/ExpressionEngineExtensions.cs b/src/WorkflowEngine.Core/ExpressionEngineExtensions.cs new file mode 100644 index 0000000..fde98f8 --- /dev/null +++ b/src/WorkflowEngine.Core/ExpressionEngineExtensions.cs @@ -0,0 +1,55 @@ +using ExpressionEngine; +using ExpressionEngine.Functions.Base; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WorkflowEngine.Core.Expressions; + +namespace WorkflowEngine.Core +{ + public static class ExpressionEngineExtensions + { + internal static IServiceCollection AddFunction(this IServiceCollection serviceCollection) + where T : class, IFunction + + { + serviceCollection.AddScoped(); + serviceCollection.AddScoped(x => x.GetRequiredService()); + return serviceCollection; + } + + public static IServiceCollection AddFunctions(this IServiceCollection services) + { + services.AddFunction(); + services.AddFunction(); + services.AddFunction(); + services.AddFunction(); + return services; + } + public static async ValueTask> ResolveInputs(this IExpressionEngine engine, ActionMetadata actionMetadata, ILogger logger) + { + + var inputs = actionMetadata.Inputs; + + foreach (var input in inputs.Keys.ToArray()) + { + if (inputs[input] is string str && str.Contains("@")) + { + inputs[input] = await engine.ParseToValueContainer(str); + } + else + { + + logger.LogWarning("{Key}: {Type}", input, inputs[input].GetType()); + } + // inputs[input] = inputs[input]; + } + + return inputs; + } + } +} diff --git a/src/WorkflowEngine.Core/Expressions/ItemsFunction.cs b/src/WorkflowEngine.Core/Expressions/ItemsFunction.cs new file mode 100644 index 0000000..8bc4c45 --- /dev/null +++ b/src/WorkflowEngine.Core/Expressions/ItemsFunction.cs @@ -0,0 +1,33 @@ +using ExpressionEngine; +using ExpressionEngine.Functions.Base; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core.Expressions +{ + public class ItemsFunction : Function + { + private readonly IOutputsRepository outputsRepository; + private readonly IRunContextAccessor runContextFactory; + + public ItemsFunction(IOutputsRepository outputsRepository, IRunContextAccessor runContextFactory) : base("items") + { + this.outputsRepository=outputsRepository; + this.runContextFactory=runContextFactory; + } + public override async ValueTask ExecuteFunction(params ValueContainer[] parameters) + { + var run = runContextFactory.RunContext; + var id = run.RunId; + + var triggerData = JToken.FromObject(await outputsRepository.GetOutputData(id, parameters[0].ToString())); + + var parsed = await ValueContainerExtension.CreateValueContainerFromJToken(triggerData); + + var body = parsed["body"]; + var item = body["item"]; + + return item; + } + } +} diff --git a/src/WorkflowEngine.Core/Expressions/OutputsFunction.cs b/src/WorkflowEngine.Core/Expressions/OutputsFunction.cs new file mode 100644 index 0000000..be7f34d --- /dev/null +++ b/src/WorkflowEngine.Core/Expressions/OutputsFunction.cs @@ -0,0 +1,37 @@ +using ExpressionEngine; +using ExpressionEngine.Functions.Base; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core.Expressions +{ + public class OutputsFunction : Function + { + private readonly IOutputsRepository outputsRepository; + private readonly IRunContextAccessor runContextFactory; + private readonly IScopeContext scopeContext; + + public OutputsFunction(IOutputsRepository outputsRepository, IRunContextAccessor runContextFactory, IScopeContext scopeContext) : base("outputs") + { + this.outputsRepository=outputsRepository; + this.runContextFactory=runContextFactory; + this.scopeContext=scopeContext; + } + public override async ValueTask ExecuteFunction(params ValueContainer[] parameters) + { + var run = runContextFactory.RunContext; + var id = run.RunId; + + var key = parameters[0].ToString(); + if (scopeContext.Scope.Contains(".")) + { + key = $"{scopeContext.Scope.Substring(0, scopeContext.Scope.LastIndexOf('.'))}.{key}"; + } + var triggerData = JToken.FromObject(await outputsRepository.GetOutputData(id, key)); + + var parsed= await ValueContainerExtension.CreateValueContainerFromJToken(triggerData); + + return parsed; + } + } +} diff --git a/src/WorkflowEngine.Core/Expressions/TriggerBodyFunction.cs b/src/WorkflowEngine.Core/Expressions/TriggerBodyFunction.cs new file mode 100644 index 0000000..94f1c87 --- /dev/null +++ b/src/WorkflowEngine.Core/Expressions/TriggerBodyFunction.cs @@ -0,0 +1,28 @@ +using ExpressionEngine; +using ExpressionEngine.Functions.Base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core.Expressions +{ + public class TriggerBodyFunction : Function + { + private readonly TriggerOutputsFunction triggerOutputsFunction; + + public TriggerBodyFunction(TriggerOutputsFunction triggerOutputsFunction) : base("triggerBody") + { + this.triggerOutputsFunction=triggerOutputsFunction; + } + public async override ValueTask ExecuteFunction(params ValueContainer[] parameters) + { + var trigger = await triggerOutputsFunction.ExecuteFunction(); + + return trigger["body"]; + + + } + } +} diff --git a/src/WorkflowEngine.Core/Expressions/TriggerOutputsFunction.cs b/src/WorkflowEngine.Core/Expressions/TriggerOutputsFunction.cs new file mode 100644 index 0000000..4d4fde2 --- /dev/null +++ b/src/WorkflowEngine.Core/Expressions/TriggerOutputsFunction.cs @@ -0,0 +1,36 @@ +using ExpressionEngine; +using ExpressionEngine.Functions.Base; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core.Expressions +{ + public interface IRunContextAccessor + { + IRunContext RunContext { get; set; } + } + public class RunContextFactory : IRunContextAccessor + { + public IRunContext RunContext { get; set; } + } + + public class TriggerOutputsFunction : Function + { + private readonly IOutputsRepository outputsRepository; + private readonly IRunContextAccessor runContextFactory; + + public TriggerOutputsFunction(IOutputsRepository outputsRepository,IRunContextAccessor runContextFactory) : base("triggerOutputs") + { + this.outputsRepository=outputsRepository; + this.runContextFactory=runContextFactory; + } + public override async ValueTask ExecuteFunction(params ValueContainer[] parameters) + { + var run = runContextFactory.RunContext; + var id = run.RunId; + var triggerData = await outputsRepository.GetTriggerData(id); + + return await ValueContainerExtension.CreateValueContainerFromJToken(JToken.FromObject(triggerData)); + } + } +} diff --git a/src/WorkflowEngine.Core/IAction.cs b/src/WorkflowEngine.Core/IAction.cs index feb0e7e..09ced53 100644 --- a/src/WorkflowEngine.Core/IAction.cs +++ b/src/WorkflowEngine.Core/IAction.cs @@ -1,12 +1,17 @@ using System; +using System.Collections.Generic; namespace WorkflowEngine.Core { - public interface IAction + public interface IAction : IRunContext { public DateTimeOffset ScheduledTime { get; set; } public string Type { get; set; } string Key { get; } + IDictionary Inputs { get; set; } + + public int Index { get; set; } + public bool ScopeMoveNext { get; set; } } diff --git a/src/WorkflowEngine.Core/IActionExecutor.cs b/src/WorkflowEngine.Core/IActionExecutor.cs index 5b79102..8d1c33e 100644 --- a/src/WorkflowEngine.Core/IActionExecutor.cs +++ b/src/WorkflowEngine.Core/IActionExecutor.cs @@ -4,7 +4,7 @@ namespace WorkflowEngine.Core { public interface IActionExecutor { - public ValueTask ExecuteAsync(IWorkflow workflow, IAction action); + public ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action); } diff --git a/src/WorkflowEngine.Core/IActionImplementation.cs b/src/WorkflowEngine.Core/IActionImplementation.cs index 71e079f..418f805 100644 --- a/src/WorkflowEngine.Core/IActionImplementation.cs +++ b/src/WorkflowEngine.Core/IActionImplementation.cs @@ -6,7 +6,7 @@ public interface IActionImplementation { - ValueTask ExecuteAsync(IWorkflow workflow, IAction action); + ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action); } diff --git a/src/WorkflowEngine.Core/IActionImplementationExtenssions.cs b/src/WorkflowEngine.Core/IActionImplementationExtenssions.cs index 0c72520..243963a 100644 --- a/src/WorkflowEngine.Core/IActionImplementationExtenssions.cs +++ b/src/WorkflowEngine.Core/IActionImplementationExtenssions.cs @@ -4,11 +4,11 @@ namespace WorkflowEngine.Core { public static class IActionImplementationExtenssions { - public static IServiceCollection AddAction(this IServiceCollection services, string type) + public static IServiceCollection AddAction(this IServiceCollection services, string type = null) where T: class, IActionImplementation { return services.AddTransient() - .AddSingleton< IActionImplementationMetadata>(new ActionImplementationMetadata { Type = type }); + .AddSingleton< IActionImplementationMetadata>(new ActionImplementationMetadata { Type = type ?? typeof(T).Name }); } } diff --git a/src/WorkflowEngine.Core/IActionResult.cs b/src/WorkflowEngine.Core/IActionResult.cs index cbdab4c..cfe00b2 100644 --- a/src/WorkflowEngine.Core/IActionResult.cs +++ b/src/WorkflowEngine.Core/IActionResult.cs @@ -3,8 +3,10 @@ //Har info om hvad der skal schedules next i hangfire public interface IActionResult { - string Key { get; } + string Key { get; } string Status { get; } + object Result { get; } + string FailedReason { get; } } diff --git a/src/WorkflowEngine.Core/IOutputsRepository.cs b/src/WorkflowEngine.Core/IOutputsRepository.cs new file mode 100644 index 0000000..4afcd1f --- /dev/null +++ b/src/WorkflowEngine.Core/IOutputsRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core +{ + public interface IOutputsRepository + { + ValueTask AddAsync(IRunContext context, IWorkflow workflow, IAction action, IActionResult result); + ValueTask AddAsync(IRunContext context, IWorkflow workflow, ITrigger trigger); + ValueTask GetTriggerData(Guid id); + ValueTask AddInput(IRunContext context, IWorkflow workflow, IAction action); + ValueTask GetOutputData(Guid id, string v); + ValueTask AddArrayItemAsync(IRunContext run, IWorkflow workflow, string key, IActionResult result); + ValueTask AddArrayInput(IRunContext context, IWorkflow workflow, IAction action); + ValueTask StartScope(IRunContext context, IWorkflow workflow, IAction action); + ValueTask AddScopeItem(IRunContext context, IWorkflow workflow, IAction action, IActionResult result); + ValueTask EndScope(IRunContext run, IWorkflow workflow, IAction action); + } + + +} diff --git a/src/WorkflowEngine.Core/ITrigger.cs b/src/WorkflowEngine.Core/ITrigger.cs index 363a1a1..d33c185 100644 --- a/src/WorkflowEngine.Core/ITrigger.cs +++ b/src/WorkflowEngine.Core/ITrigger.cs @@ -1,13 +1,25 @@ using System; +using System.Collections.Generic; namespace WorkflowEngine.Core { public interface ITrigger + { + DateTimeOffset ScheduledTime { get; set; } + string Type { get; set; } + + IDictionary Inputs { get; set; } + + string Key { get; } + } + public class Trigger : ITrigger { public DateTimeOffset ScheduledTime { get; set; } public string Type { get; set; } - } + public IDictionary Inputs { get; set; } = new Dictionary(); + public string Key { get; set; } + } } diff --git a/src/WorkflowEngine.Core/ITriggerContext.cs b/src/WorkflowEngine.Core/ITriggerContext.cs index ca141d0..e14658d 100644 --- a/src/WorkflowEngine.Core/ITriggerContext.cs +++ b/src/WorkflowEngine.Core/ITriggerContext.cs @@ -1,8 +1,13 @@ -namespace WorkflowEngine.Core +using System; + +namespace WorkflowEngine.Core { - public interface ITriggerContext + public interface ITriggerContext:IRunContext { IWorkflow Workflow { get; } + ITrigger Trigger { get; set; } + + } diff --git a/src/WorkflowEngine.Core/IWorkflowExecutor.cs b/src/WorkflowEngine.Core/IWorkflowExecutor.cs index 4a0c062..8b0e35a 100644 --- a/src/WorkflowEngine.Core/IWorkflowExecutor.cs +++ b/src/WorkflowEngine.Core/IWorkflowExecutor.cs @@ -1,11 +1,16 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace WorkflowEngine.Core { + public interface IRunContext + { + Guid RunId { get; set; } + } public interface IWorkflowExecutor { public ValueTask Trigger(ITriggerContext context); - public ValueTask GetNextAction(IWorkflow workflow, IActionResult priorResult); + public ValueTask GetNextAction(IRunContext context, IWorkflow workflow, IActionResult priorResult); } diff --git a/src/WorkflowEngine.Core/TriggerContext.cs b/src/WorkflowEngine.Core/TriggerContext.cs index 01f5bc1..e7e2ee3 100644 --- a/src/WorkflowEngine.Core/TriggerContext.cs +++ b/src/WorkflowEngine.Core/TriggerContext.cs @@ -5,13 +5,21 @@ namespace WorkflowEngine.Core public class TriggerContext : ITriggerContext, IFormattable { public IWorkflow Workflow { get; set; } + public ITrigger Trigger { get; set; } + + public Guid RunId { get; set; } + public string ToString(string format, IFormatProvider formatProvider) { - if(format =="WorkFlow") + if(format =="Workflow") { - return Workflow?.GetType().Name.ToString(); + return $"{Workflow?.GetType().Name} workflow={Workflow?.Id} version={Workflow?.Version} trigger={Trigger?.Key}"; } + + if (format == "Id") + return RunId.ToString(); + return string.Empty; } } diff --git a/src/WorkflowEngine.Core/TriggerMetadata.cs b/src/WorkflowEngine.Core/TriggerMetadata.cs index b0259b7..0063cfd 100644 --- a/src/WorkflowEngine.Core/TriggerMetadata.cs +++ b/src/WorkflowEngine.Core/TriggerMetadata.cs @@ -4,7 +4,7 @@ namespace WorkflowEngine.Core { public class TriggerMetadata { - public Dictionary Inputs { get; set; } = new Dictionary(); + public string Type { get; set; } } diff --git a/src/WorkflowEngine.Core/WorkflowActions.cs b/src/WorkflowEngine.Core/WorkflowActions.cs index 183bbf6..60fcff2 100644 --- a/src/WorkflowEngine.Core/WorkflowActions.cs +++ b/src/WorkflowEngine.Core/WorkflowActions.cs @@ -1,10 +1,69 @@ using System.Collections.Generic; +using System.Linq; namespace WorkflowEngine.Core { + public class ForLoopActionMetadata : ActionMetadata, IScopedActionMetadata + { + public object ForEach { get; set; } + + public WorkflowActions Actions { get; set; } = new WorkflowActions(); + + } + public interface IScopedActionMetadata + { + WorkflowActions Actions { get; set; } + } public class WorkflowActions : Dictionary { + public ActionMetadata FindAction(string key) + { + if (key.Contains(".")) + { + var leg = key.Substring(0, key.IndexOf('.')); + var child = this[leg]; + if(child is IScopedActionMetadata childactions) + { + return childactions.Actions.FindAction(key.Substring(leg.Length+1)); + } + } + + if(ContainsKey(key)) + return this[key]; + + return null; + } + public KeyValuePair FindNextAction(string key) + { + if (key.Contains(".")) + { + var leg = key.Substring(0, key.IndexOf('.')); + var child = this[leg]; + if (child is IScopedActionMetadata childactions) + { + var a= childactions.Actions.FindNextAction(key.Substring(leg.Length+1)); + if (a.IsDefault()) + return a; + + + return new KeyValuePair(leg+"."+a.Key, a.Value); + } + } + + var next = this.SingleOrDefault(c => c.Value.RunAfter.ContainsKey(key)); + + return next; + } + + public ActionMetadata FindParentAction(string key) + { + if (key.Contains(".")) + { + return FindAction(key.Substring(0, key.LastIndexOf('.'))); + } + return null; + } } diff --git a/src/WorkflowEngine.Core/WorkflowEngine.Core.csproj b/src/WorkflowEngine.Core/WorkflowEngine.Core.csproj index 15ee331..3aaa8a4 100644 --- a/src/WorkflowEngine.Core/WorkflowEngine.Core.csproj +++ b/src/WorkflowEngine.Core/WorkflowEngine.Core.csproj @@ -1,8 +1,8 @@ - + - netcoreapp3.1 - + netcoreapp3.1;net5.0;net6.0 + Delegate.WorkflowEngine.Core Delegate A/S WorkflowEngine used to execute workflows based on Json description. @@ -10,8 +10,13 @@ - - + + + + + + + diff --git a/src/WorkflowEngine.Core/WorkflowExecutor.cs b/src/WorkflowEngine.Core/WorkflowExecutor.cs index 1fa91fc..d890f47 100644 --- a/src/WorkflowEngine.Core/WorkflowExecutor.cs +++ b/src/WorkflowEngine.Core/WorkflowExecutor.cs @@ -18,35 +18,47 @@ public static bool IsDefault(this T value) where T : struct public class WorkflowExecutor : IWorkflowExecutor { private readonly ILogger logger; + private readonly IOutputsRepository outputsRepository; - public WorkflowExecutor(ILogger logger) + public WorkflowExecutor(ILogger logger, IOutputsRepository outputsRepository) { - this.logger = logger; + this.logger = logger??throw new ArgumentNullException(nameof(logger)); + this.outputsRepository=outputsRepository??throw new ArgumentNullException(nameof(outputsRepository)); } - public ValueTask GetNextAction(IWorkflow workflow, IActionResult priorResult) + public ValueTask GetNextAction(IRunContext context, IWorkflow workflow, IActionResult priorResult) { - logger.LogInformation("Finding Next Action for {WorkflowId} and prior {@result}", workflow.Id, priorResult); + logger.LogInformation("Finding Next Action for {WorkflowId} and prior {@result} ", workflow.Id, priorResult); //var action = workflow.Manifest.Actions.Single(c => c.Key == priorResult.Key); - var next = workflow.Manifest.Actions.SingleOrDefault(c => c.Value.RunAfter.ContainsKey(priorResult.Key)); + + var next = workflow.Manifest.Actions.FindNextAction(priorResult.Key); + //var parent = workflow.Manifest.Actions.FindParentAction(priorResult.Key) is ForLoopActionMetadata; + if (next.IsDefault()) return new ValueTask(); - if (next.Value.RunAfter[priorResult.Key].Contains(priorResult.Status)) + if (next.Value.ShouldRun(priorResult.Key,priorResult.Status)) // .RunAfter[priorResult.Key].Contains(priorResult.Status)) { - return new ValueTask(new Action { Type = next.Value.Type, Key=next.Key, ScheduledTime=DateTimeOffset.UtcNow }); + return new ValueTask(new Action { RunId=context.RunId, Type = next.Value.Type, Key=next.Key, ScheduledTime=DateTimeOffset.UtcNow }); } return new ValueTask(); } - public ValueTask Trigger(ITriggerContext context) + + + public async ValueTask Trigger(ITriggerContext context) { + + await outputsRepository.AddAsync(context,context.Workflow, context.Trigger); + var action = context.Workflow.Manifest.Actions.SingleOrDefault(c => c.Value.RunAfter?.Count == 0); + + if (action.IsDefault()) - return new ValueTask(); + return null; - return new ValueTask(new Action { Type = action.Value.Type, Key=action.Key, ScheduledTime = DateTimeOffset.UtcNow }); + return new Action { Type = action.Value.Type, Key=action.Key, ScheduledTime = DateTimeOffset.UtcNow, RunId = context.RunId }; } } diff --git a/src/WorkflowEngine.Hangfire/Class1.cs b/src/WorkflowEngine.Hangfire/Class1.cs deleted file mode 100644 index 3fd3366..0000000 --- a/src/WorkflowEngine.Hangfire/Class1.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Threading.Tasks; -using WorkflowEngine.Core; - -namespace WorkflowEngine -{ - - public class HangfireWorkflowExecutor : IHangfireWorkflowExecutor, IHangfireActionExecutor - { - private readonly IWorkflowExecutor executor; - private readonly IActionExecutor actionExecutor; - - public HangfireWorkflowExecutor(IWorkflowExecutor executor, IActionExecutor actionExecutor) - { - this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); - this.actionExecutor = actionExecutor ?? throw new ArgumentNullException(nameof(actionExecutor)); - } - - /// - /// Runs on the background process in hangfire - /// - /// - /// - /// - - public async ValueTask ExecuteAsync(string type, IWorkflow workflow, IAction action) - { - var result = await actionExecutor.ExecuteAsync(workflow, action); - - if (result != null) - { - var next = await executor.GetNextAction(workflow, result); - - if (next != null) - { - var a = Hangfire.BackgroundJob.Enqueue( - (executor) => executor.ExecuteAsync(next.Type, workflow, next)); - } - } - - return result; - } - /// - /// Runs on the background process in hangfire - /// - /// - /// - public async ValueTask TriggerAsync(ITriggerContext context) - { - var action = await executor.Trigger(context); - - if (action != null) - { - - var a = Hangfire.BackgroundJob.Enqueue( - (executor) => executor.ExecuteAsync(action.Type, context.Workflow, action)); - } - return action; - } - } - -} diff --git a/src/WorkflowEngine.Hangfire/ForloopAction.cs b/src/WorkflowEngine.Hangfire/ForloopAction.cs new file mode 100644 index 0000000..1913a19 --- /dev/null +++ b/src/WorkflowEngine.Hangfire/ForloopAction.cs @@ -0,0 +1,65 @@ +using ExpressionEngine; +using Hangfire; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WorkflowEngine.Core.Actions +{ + public interface IArrayContext + { + public string JobId { get; set; } + } + public class ArrayContext : IArrayContext + { + public string JobId { get; set; } + } + + public class ForeachAction : IActionImplementation + { + private readonly IExpressionEngine expressionEngine; + private readonly IBackgroundJobClient backgroundJobClient; + private readonly IArrayContext arrayContext; + + public ForeachAction(IExpressionEngine expressionEngine, IBackgroundJobClient backgroundJobClient, IArrayContext arrayContext) + { + this.expressionEngine=expressionEngine??throw new ArgumentNullException(nameof(expressionEngine)); + this.backgroundJobClient=backgroundJobClient; + this.arrayContext=arrayContext??throw new ArgumentNullException(nameof(arrayContext)); + } + public async ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action) + { + var loop = workflow.Manifest.Actions.FindAction(action.Key) as ForLoopActionMetadata; + + if (loop.ForEach is string str && str.Contains("@")) + { + var items = await expressionEngine.ParseToValueContainer(str); + + if (items.Type() == ExpressionEngine.ValueType.Array) + { + var aa = items.GetValue>(); + if (action.Index < aa.Count) + { + // var nextAction = new Action { Type = action.Type, Key=action.Key, ScheduledTime = DateTimeOffset.UtcNow, RunId = context.RunId, Index = action.Index+1 }; + + var nextactionmetadata = loop.Actions.SingleOrDefault(c => c.Value.RunAfter?.Count == 0); + var nextaction = new Action { Type = nextactionmetadata.Value.Type, Key= $"{action.Key}.{nextactionmetadata.Key}", ScheduledTime = DateTimeOffset.UtcNow, RunId = context.RunId, Index=action.Index }; + + var a = backgroundJobClient.ContinueJobWith(arrayContext.JobId, + (executor) => executor.ExecuteAsync(context, workflow, nextaction,null)); + + return new { item = aa[action.Index] }; + } + + } + } + + + + + return new { }; + } + } +} diff --git a/src/WorkflowEngine.Hangfire/HangfireWorkflowExecutor.cs b/src/WorkflowEngine.Hangfire/HangfireWorkflowExecutor.cs new file mode 100644 index 0000000..d2c5a0b --- /dev/null +++ b/src/WorkflowEngine.Hangfire/HangfireWorkflowExecutor.cs @@ -0,0 +1,92 @@ +using Hangfire; +using Hangfire.Server; +using System; +using System.Threading.Tasks; +using WorkflowEngine.Core; +using WorkflowEngine.Core.Actions; +using WorkflowEngine.Core.Expressions; +using Action = WorkflowEngine.Core.Action; + +namespace WorkflowEngine +{ + + public class HangfireWorkflowExecutor : IHangfireWorkflowExecutor, IHangfireActionExecutor + { + private readonly IBackgroundJobClient backgroundJobClient; + private readonly IRunContextAccessor runContextAccessor; + private readonly IWorkflowExecutor executor; + private readonly IActionExecutor actionExecutor; + private readonly IOutputsRepository outputRepository; + private readonly IArrayContext arrayContext; + + public HangfireWorkflowExecutor(IBackgroundJobClient backgroundJobClient, IArrayContext arrayContext, IRunContextAccessor runContextAccessor, IWorkflowExecutor executor, IActionExecutor actionExecutor, IOutputsRepository actionResultRepository) + { + this.backgroundJobClient=backgroundJobClient??throw new ArgumentNullException(nameof(backgroundJobClient)); + this.arrayContext=arrayContext??throw new ArgumentNullException(nameof(arrayContext)); + this.runContextAccessor=runContextAccessor; + this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); + this.actionExecutor = actionExecutor ?? throw new ArgumentNullException(nameof(actionExecutor)); + this.outputRepository=actionResultRepository; + } + + /// + /// Runs on the background process in hangfire + /// + /// + /// + /// + + public async ValueTask ExecuteAsync(IRunContext run, IWorkflow workflow, IAction action, PerformContext context) + { + runContextAccessor.RunContext = run; + arrayContext.JobId=context.BackgroundJob.Id; + + + var result = await actionExecutor.ExecuteAsync(run, workflow, action); + + + + if (result != null) + { + var next = await executor.GetNextAction(run, workflow, result); + + if (next != null) + { + var a = backgroundJobClient.Enqueue( + (executor) => executor.ExecuteAsync(run, workflow, next,null)); + }else if(workflow.Manifest.Actions.FindParentAction(action.Key) is ForLoopActionMetadata scope) + { + + var scopeaction= new Action {ScopeMoveNext=true, RunId=run.RunId, Type = scope.Type, Key=action.Key.Substring(0, action.Key.LastIndexOf('.')), ScheduledTime=DateTimeOffset.UtcNow }; + + + var a = backgroundJobClient.Enqueue( + (executor) => executor.ExecuteAsync(run, workflow, scopeaction, null)); + + //await outputRepository.EndScope(run, workflow, action); + } + } + + return result; + } + /// + /// Runs on the background process in hangfire + /// + /// + /// + public async ValueTask TriggerAsync(ITriggerContext context) + { + runContextAccessor.RunContext = context; + var action = await executor.Trigger(context); + + if (action != null) + { + + var a = backgroundJobClient.Enqueue( + (executor) => executor.ExecuteAsync(context, context.Workflow, action,null)); + } + return action; + } + } + +} diff --git a/src/WorkflowEngine.Hangfire/IHangfireActionExecutor.cs b/src/WorkflowEngine.Hangfire/IHangfireActionExecutor.cs index f69be58..0f897a6 100644 --- a/src/WorkflowEngine.Hangfire/IHangfireActionExecutor.cs +++ b/src/WorkflowEngine.Hangfire/IHangfireActionExecutor.cs @@ -1,4 +1,5 @@ using Hangfire; +using Hangfire.Server; using System.ComponentModel; using System.Threading.Tasks; using WorkflowEngine.Core; @@ -7,8 +8,8 @@ namespace WorkflowEngine { public interface IHangfireActionExecutor { - [JobDisplayName("Action: {0}, workflow={1:Id}")] - public ValueTask ExecuteAsync(string type, IWorkflow workflow, IAction action); + [JobDisplayName("Action: {2:Type}, RunId={0:Id} workflow={1:Id}")] + public ValueTask ExecuteAsync(IRunContext context, IWorkflow workflow, IAction action, PerformContext hangfireContext); } } diff --git a/src/WorkflowEngine.Hangfire/IHangfireWorkflowExecutor.cs b/src/WorkflowEngine.Hangfire/IHangfireWorkflowExecutor.cs index b7b237a..b31d60d 100644 --- a/src/WorkflowEngine.Hangfire/IHangfireWorkflowExecutor.cs +++ b/src/WorkflowEngine.Hangfire/IHangfireWorkflowExecutor.cs @@ -7,7 +7,7 @@ namespace WorkflowEngine { public interface IHangfireWorkflowExecutor { - [JobDisplayName("Trigger: {0:WorkFlow}")] + [JobDisplayName("Trigger: {0:Workflow} RunId={0:Id}")] public ValueTask TriggerAsync(ITriggerContext context); } From b62648687292029f503356a7718131cf37633c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Poul=20Kjeldager=20S=C3=B8rensen?= Date: Thu, 17 Feb 2022 04:31:04 +0100 Subject: [PATCH 2/4] feat!: Scope Support Added for forloops --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3ce53a..dde4e05 100644 --- a/README.md +++ b/README.md @@ -129,4 +129,6 @@ Service Registration } } }); -``` \ No newline at end of file +``` + +### TODO Show Loop example \ No newline at end of file From e21dd2dc3c91086cdb20a0e516621352a861adca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Poul=20Kjeldager=20S=C3=B8rensen?= Date: Thu, 17 Feb 2022 04:40:23 +0100 Subject: [PATCH 3/4] updated deps and fixed build pipeline --- .github/workflows/build.yml | 8 ++++++-- .github/workflows/prerelease.yml | 6 +++++- .github/workflows/release.yml | 8 ++++++++ .../WorkflowEngine.Hangfire.csproj | 6 +++--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 621a6c0..ef9630e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,11 @@ jobs: steps: - name: Checkout code base uses: actions/checkout@v2 - + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + - name: Run tests run: dotnet test --verbosity normal -f ${{ matrix.dotnet }} @@ -33,7 +37,7 @@ jobs: - uses: actions/setup-dotnet@v1 with: - dotnet-version: '5.0.x' + dotnet-version: '6.0.x' - name: Cleaning run: dotnet clean diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 73f7bcd..9780c03 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -12,7 +12,11 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v2 - + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + - name: Setup Node.js uses: actions/setup-node@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1befb9d..e0e0d04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,10 @@ jobs: - name: Checkout code base uses: actions/checkout@v2 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + - name: Run tests run: dotnet test --verbosity normal -f ${{ matrix.dotnet }} @@ -29,6 +33,10 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' - name: Setup Node.js uses: actions/setup-node@v1 diff --git a/src/WorkflowEngine.Hangfire/WorkflowEngine.Hangfire.csproj b/src/WorkflowEngine.Hangfire/WorkflowEngine.Hangfire.csproj index 7b4e0e6..61345ca 100644 --- a/src/WorkflowEngine.Hangfire/WorkflowEngine.Hangfire.csproj +++ b/src/WorkflowEngine.Hangfire/WorkflowEngine.Hangfire.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + netcoreapp3.1;net5.0;net6.0 Delegate.WorkflowEngine.Hangfire Delegate A/S @@ -10,11 +10,11 @@ - + - + From 04cd914e3f478d0f95ccf22488564a9d8f1954a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Poul=20Kjeldager=20S=C3=B8rensen?= Date: Thu, 17 Feb 2022 04:40:59 +0100 Subject: [PATCH 4/4] updated demo app deps --- apps/WorkflowEngine.DemoApp/WorkflowEngine.DemoApp.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/WorkflowEngine.DemoApp/WorkflowEngine.DemoApp.csproj b/apps/WorkflowEngine.DemoApp/WorkflowEngine.DemoApp.csproj index e94c306..1f72a9d 100644 --- a/apps/WorkflowEngine.DemoApp/WorkflowEngine.DemoApp.csproj +++ b/apps/WorkflowEngine.DemoApp/WorkflowEngine.DemoApp.csproj @@ -7,12 +7,12 @@ - + - - + +