From cedc15176d0bd195980bc5f1c6dfe0ff4e001aca Mon Sep 17 00:00:00 2001 From: glucaci Date: Sun, 14 Feb 2021 11:48:00 +0100 Subject: [PATCH 01/11] Execute post middleware in any case --- src/WorkflowCore/Services/WorkflowExecutor.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs index c6241fd64..0bdf8ce36 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -212,7 +212,10 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo workflow.NextExecution = null; if (workflow.Status == WorkflowStatus.Complete) + { + await RunPostMiddleware(workflow, def); return; + } foreach (var pointer in workflow.ExecutionPointers.Where(x => x.Active && (x.Children ?? new List()).Count == 0)) { @@ -242,18 +245,17 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo } if ((workflow.NextExecution != null) || (workflow.ExecutionPointers.Any(x => x.EndTime == null))) + { + await RunPostMiddleware(workflow, def); return; + } workflow.Status = WorkflowStatus.Complete; workflow.CompleteTime = _datetimeProvider.UtcNow; - using (var scope = _serviceProvider.CreateScope()) - { - var middlewareRunner = scope.ServiceProvider.GetRequiredService(); - await middlewareRunner.RunPostMiddleware(workflow, def); - } + await RunPostMiddleware(workflow, def); - _publisher.PublishNotification(new WorkflowCompleted() + _publisher.PublishNotification(new WorkflowCompleted { EventTimeUtc = _datetimeProvider.UtcNow, Reference = workflow.Reference, @@ -262,5 +264,14 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo Version = workflow.Version }); } + + private Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + { + using (var scope = _serviceProvider.CreateScope()) + { + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + return middlewareRunner.RunPostMiddleware(workflow, def); + } + } } } From f462d6e4f815f2e879c6cb78178f090a49baf4e9 Mon Sep 17 00:00:00 2001 From: glucaci Date: Sun, 14 Feb 2021 11:59:15 +0100 Subject: [PATCH 02/11] Add post middleware in more places --- src/WorkflowCore/Services/WorkflowExecutor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs index 0bdf8ce36..01ce4c924 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -222,6 +222,7 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo if (!pointer.SleepUntil.HasValue) { workflow.NextExecution = 0; + await RunPostMiddleware(workflow, def); return; } @@ -237,6 +238,7 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo if (!pointer.SleepUntil.HasValue) { workflow.NextExecution = 0; + await RunPostMiddleware(workflow, def); return; } From e15b2a6afcfb587908e240975cf9120c513573dc Mon Sep 17 00:00:00 2001 From: glucaci Date: Mon, 15 Feb 2021 08:00:05 +0100 Subject: [PATCH 03/11] Cleanup --- src/WorkflowCore/Services/WorkflowExecutor.cs | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs index 01ce4c924..ee58e32bb 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -96,7 +96,13 @@ public async Task Execute(WorkflowInstance workflow, Can _cancellationProcessor.ProcessCancellations(workflow, def, wfResult); } ProcessAfterExecutionIteration(workflow, def, wfResult); - await DetermineNextExecutionTime(workflow, def); + DetermineNextExecutionTime(workflow, def); + + using (var scope = _serviceProvider.CreateScope()) + { + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + await middlewareRunner.RunPostMiddleware(workflow, def); + } return wfResult; } @@ -206,14 +212,13 @@ private void ProcessAfterExecutionIteration(WorkflowInstance workflow, WorkflowD } } - private async Task DetermineNextExecutionTime(WorkflowInstance workflow, WorkflowDefinition def) + private void DetermineNextExecutionTime(WorkflowInstance workflow, WorkflowDefinition def) { //TODO: move to own class workflow.NextExecution = null; if (workflow.Status == WorkflowStatus.Complete) { - await RunPostMiddleware(workflow, def); return; } @@ -222,7 +227,6 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo if (!pointer.SleepUntil.HasValue) { workflow.NextExecution = 0; - await RunPostMiddleware(workflow, def); return; } @@ -238,7 +242,6 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo if (!pointer.SleepUntil.HasValue) { workflow.NextExecution = 0; - await RunPostMiddleware(workflow, def); return; } @@ -248,15 +251,12 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo if ((workflow.NextExecution != null) || (workflow.ExecutionPointers.Any(x => x.EndTime == null))) { - await RunPostMiddleware(workflow, def); return; } workflow.Status = WorkflowStatus.Complete; workflow.CompleteTime = _datetimeProvider.UtcNow; - await RunPostMiddleware(workflow, def); - _publisher.PublishNotification(new WorkflowCompleted { EventTimeUtc = _datetimeProvider.UtcNow, @@ -266,14 +266,5 @@ private async Task DetermineNextExecutionTime(WorkflowInstance workflow, Workflo Version = workflow.Version }); } - - private Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) - { - using (var scope = _serviceProvider.CreateScope()) - { - var middlewareRunner = scope.ServiceProvider.GetRequiredService(); - return middlewareRunner.RunPostMiddleware(workflow, def); - } - } } } From 98df2bbac23c0b69934c8666269e77adb88b9ea9 Mon Sep 17 00:00:00 2001 From: glucaci Date: Thu, 15 Apr 2021 23:41:57 +0300 Subject: [PATCH 04/11] Add new WorkflowMiddlewarePhase --- .../Interface/IWorkflowMiddleware.cs | 7 +++- .../Interface/IWorkflowMiddlewareRunner.cs | 13 ++++++- src/WorkflowCore/Services/WorkflowExecutor.cs | 12 +++++-- .../Services/WorkflowMiddlewareRunner.cs | 35 ++++++------------- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/WorkflowCore/Interface/IWorkflowMiddleware.cs b/src/WorkflowCore/Interface/IWorkflowMiddleware.cs index 71781b30d..ede4ca8ec 100644 --- a/src/WorkflowCore/Interface/IWorkflowMiddleware.cs +++ b/src/WorkflowCore/Interface/IWorkflowMiddleware.cs @@ -16,7 +16,12 @@ public enum WorkflowMiddlewarePhase /// /// The middleware should run after a workflow completes. /// - PostWorkflow + PostWorkflow, + + /// + /// The middleware should run after each workflow execution. + /// + ExecuteWorkflow } /// diff --git a/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs b/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs index 96aab8b74..6c47899f5 100644 --- a/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs +++ b/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs @@ -4,7 +4,7 @@ namespace WorkflowCore.Interface { /// - /// Runs workflow pre/post middleware. + /// Runs workflow pre/post and execute middleware. /// public interface IWorkflowMiddlewareRunner { @@ -29,5 +29,16 @@ public interface IWorkflowMiddlewareRunner /// The definition. /// A task that will complete when all middleware has run. Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def); + + /// + /// Runs workflow-level middleware that is set to run at the + /// phase. Middleware will be run in the + /// order in which they were registered with DI with middleware declared earlier starting earlier and + /// completing later. + /// + /// The to run for. + /// The definition. + /// A task that will complete when all middleware has run. + Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def); } } diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs index ee58e32bb..5c72355a4 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -96,12 +96,12 @@ public async Task Execute(WorkflowInstance workflow, Can _cancellationProcessor.ProcessCancellations(workflow, def, wfResult); } ProcessAfterExecutionIteration(workflow, def, wfResult); - DetermineNextExecutionTime(workflow, def); + await DetermineNextExecutionTime(workflow, def); using (var scope = _serviceProvider.CreateScope()) { var middlewareRunner = scope.ServiceProvider.GetRequiredService(); - await middlewareRunner.RunPostMiddleware(workflow, def); + await middlewareRunner.RunExecuteMiddleware(workflow, def); } return wfResult; @@ -212,7 +212,7 @@ private void ProcessAfterExecutionIteration(WorkflowInstance workflow, WorkflowD } } - private void DetermineNextExecutionTime(WorkflowInstance workflow, WorkflowDefinition def) + private async Task DetermineNextExecutionTime(WorkflowInstance workflow, WorkflowDefinition def) { //TODO: move to own class workflow.NextExecution = null; @@ -257,6 +257,12 @@ private void DetermineNextExecutionTime(WorkflowInstance workflow, WorkflowDefin workflow.Status = WorkflowStatus.Complete; workflow.CompleteTime = _datetimeProvider.UtcNow; + using (var scope = _serviceProvider.CreateScope()) + { + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + await middlewareRunner.RunPostMiddleware(workflow, def); + } + _publisher.PublishNotification(new WorkflowCompleted { EventTimeUtc = _datetimeProvider.UtcNow, diff --git a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs index 6eb6968b8..9b6eea445 100644 --- a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs +++ b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Services { - /// + /// public class WorkflowMiddlewareRunner : IWorkflowMiddlewareRunner { private static readonly WorkflowDelegate NoopWorkflowDelegate = () => Task.CompletedTask; @@ -17,23 +17,13 @@ public class WorkflowMiddlewareRunner : IWorkflowMiddlewareRunner public WorkflowMiddlewareRunner( IEnumerable middleware, - IServiceProvider serviceProvider - ) + IServiceProvider serviceProvider) { _middleware = middleware; _serviceProvider = serviceProvider; } - - /// - /// Runs workflow-level middleware that is set to run at the - /// phase. Middleware will be run in the - /// order in which they were registered with DI with middleware declared earlier starting earlier and - /// completing later. - /// - /// The to run for. - /// The definition. - /// A task that will complete when all middleware has run. + /// public async Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition def) { var preMiddleware = _middleware @@ -43,15 +33,7 @@ public async Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition await RunWorkflowMiddleware(workflow, preMiddleware); } - /// - /// Runs workflow-level middleware that is set to run at the - /// phase. Middleware will be run in the - /// order in which they were registered with DI with middleware declared earlier starting earlier and - /// completing later. - /// - /// The to run for. - /// The definition. - /// A task that will complete when all middleware has run. + /// public async Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) { var postMiddleware = _middleware @@ -77,10 +59,15 @@ public async Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinitio } } + /// + public Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + { + throw new NotImplementedException(); + } + private static async Task RunWorkflowMiddleware( WorkflowInstance workflow, - IEnumerable middlewareCollection - ) + IEnumerable middlewareCollection) { // Build the middleware chain var middlewareChain = middlewareCollection From 076d1b8dfbe25f8421f6642f45b1da86ae5927ee Mon Sep 17 00:00:00 2001 From: glucaci Date: Fri, 16 Apr 2021 00:03:27 +0300 Subject: [PATCH 05/11] Add execute middleware implementation --- .../Services/WorkflowMiddlewareRunner.cs | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs index 9b6eea445..d22470412 100644 --- a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs +++ b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs @@ -27,8 +27,7 @@ public WorkflowMiddlewareRunner( public async Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition def) { var preMiddleware = _middleware - .Where(m => m.Phase == WorkflowMiddlewarePhase.PreWorkflow) - .ToArray(); + .Where(m => m.Phase == WorkflowMiddlewarePhase.PreWorkflow); await RunWorkflowMiddleware(workflow, preMiddleware); } @@ -37,47 +36,58 @@ public async Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition public async Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) { var postMiddleware = _middleware - .Where(m => m.Phase == WorkflowMiddlewarePhase.PostWorkflow) - .ToArray(); - + .Where(m => m.Phase == WorkflowMiddlewarePhase.PostWorkflow); try { await RunWorkflowMiddleware(workflow, postMiddleware); } catch (Exception exception) { - // On error, determine which error handler to run and then run it + // TODO: + // OnPostMiddlewareError should be IWorkflowMiddlewareErrorHandler + // because we don't know to run other error handler type var errorHandlerType = def.OnPostMiddlewareError ?? typeof(IWorkflowMiddlewareErrorHandler); - using (var scope = _serviceProvider.CreateScope()) - { - var typeInstance = scope.ServiceProvider.GetService(errorHandlerType); - if (typeInstance != null && typeInstance is IWorkflowMiddlewareErrorHandler handler) - { - await handler.HandleAsync(exception); - } - } + await HandleWorkflowMiddlewareError(exception); } } /// - public Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + public async Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + { + var executeMiddleware = _middleware + .Where(m => m.Phase == WorkflowMiddlewarePhase.ExecuteWorkflow); + + try + { + await RunWorkflowMiddleware(workflow, executeMiddleware); + } + catch (Exception exception) + { + await HandleWorkflowMiddlewareError(exception); + } + } + + private async Task HandleWorkflowMiddlewareError(Exception exception) { - throw new NotImplementedException(); + using (var scope = _serviceProvider.CreateScope()) + { + var handler = scope.ServiceProvider.GetService(); + if (handler != null) + { + await handler.HandleAsync(exception); + } + } } - private static async Task RunWorkflowMiddleware( + private static Task RunWorkflowMiddleware( WorkflowInstance workflow, IEnumerable middlewareCollection) { - // Build the middleware chain - var middlewareChain = middlewareCollection + return middlewareCollection .Reverse() - .Aggregate( - NoopWorkflowDelegate, - (previous, middleware) => () => middleware.HandleAsync(workflow, previous) - ); - - await middlewareChain(); + .Aggregate(NoopWorkflowDelegate, + (previous, middleware) => + () => middleware.HandleAsync(workflow, previous))(); } } } From 2f28bd5b000230b4da0e5e22a09da24dbfe63556 Mon Sep 17 00:00:00 2001 From: glucaci Date: Fri, 16 Apr 2021 00:20:13 +0300 Subject: [PATCH 06/11] Add custom error handler for execute workflow middleware and tests --- src/WorkflowCore/Models/WorkflowDefinition.cs | 1 + .../Services/WorkflowMiddlewareRunner.cs | 34 +++--- .../Services/WorkflowMiddlewareRunnerTests.cs | 114 ++++++++++++++++++ 3 files changed, 131 insertions(+), 18 deletions(-) diff --git a/src/WorkflowCore/Models/WorkflowDefinition.cs b/src/WorkflowCore/Models/WorkflowDefinition.cs index 0cea6cc40..d40fc5a2e 100644 --- a/src/WorkflowCore/Models/WorkflowDefinition.cs +++ b/src/WorkflowCore/Models/WorkflowDefinition.cs @@ -18,6 +18,7 @@ public class WorkflowDefinition public WorkflowErrorHandling DefaultErrorBehavior { get; set; } public Type OnPostMiddlewareError { get; set; } + public Type OnExecuteMiddlewareError { get; set; } public TimeSpan? DefaultErrorRetryInterval { get; set; } diff --git a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs index d22470412..872721849 100644 --- a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs +++ b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs @@ -43,11 +43,7 @@ public async Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinitio } catch (Exception exception) { - // TODO: - // OnPostMiddlewareError should be IWorkflowMiddlewareErrorHandler - // because we don't know to run other error handler type - var errorHandlerType = def.OnPostMiddlewareError ?? typeof(IWorkflowMiddlewareErrorHandler); - await HandleWorkflowMiddlewareError(exception); + await HandleWorkflowMiddlewareError(def.OnPostMiddlewareError, exception); } } @@ -63,19 +59,7 @@ public async Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefini } catch (Exception exception) { - await HandleWorkflowMiddlewareError(exception); - } - } - - private async Task HandleWorkflowMiddlewareError(Exception exception) - { - using (var scope = _serviceProvider.CreateScope()) - { - var handler = scope.ServiceProvider.GetService(); - if (handler != null) - { - await handler.HandleAsync(exception); - } + await HandleWorkflowMiddlewareError(def.OnExecuteMiddlewareError, exception); } } @@ -89,5 +73,19 @@ private static Task RunWorkflowMiddleware( (previous, middleware) => () => middleware.HandleAsync(workflow, previous))(); } + + private async Task HandleWorkflowMiddlewareError(Type middlewareErrorType, Exception exception) + { + var errorHandlerType = middlewareErrorType ?? typeof(IWorkflowMiddlewareErrorHandler); + + using (var scope = _serviceProvider.CreateScope()) + { + var typeInstance = scope.ServiceProvider.GetService(errorHandlerType); + if (typeInstance is IWorkflowMiddlewareErrorHandler handler) + { + await handler.HandleAsync(exception); + } + } + } } } diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs b/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs index 991acfb23..0e9c0da07 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs @@ -238,6 +238,120 @@ public async Task .MustHaveHappenedOnceExactly(); } + [Fact(DisplayName = "RunExecuteMiddleware should run nothing when no middleware")] + public void RunExecuteMiddleware_should_run_nothing_when_no_middleware() + { + // Act + Func action = async () => await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + action.ShouldNotThrow(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should run middleware when one middleware")] + public async Task RunExecuteMiddleware_should_run_middleware_when_one_middleware() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow); + Middleware.Add(middleware); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should run all middleware when multiple middleware")] + public async Task RunExecuteMiddleware_should_run_all_middleware_when_multiple_middleware() + { + // Arrange + var middleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + var middleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 2); + var middleware3 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 3); + Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); + + // Act + await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware3)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(middleware2)) + .MustHaveHappenedOnceExactly()) + .Then(A + .CallTo(HandleMethodFor(middleware1)) + .MustHaveHappenedOnceExactly()); + } + + [Fact(DisplayName = "RunExecuteMiddleware should run middleware in ExecuteWorkflow and PostWorkflow phase")] + public async Task RunExecuteMiddleware_should_run_middleware_in_ExecuteWorkflow_and_PostWorkflow_phase() + { + // Arrange + var executeMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + var postMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 2); + var preMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 3); + Middleware.AddRange(new[] { preMiddleware, postMiddleware, executeMiddleware }); + + // Act + // TODO: add same test when workflow not completed + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(executeMiddleware)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(postMiddleware)) + .MustHaveHappenedOnceExactly()); + + A.CallTo(HandleMethodFor(preMiddleware)).MustNotHaveHappened(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should call top level error handler when middleware throws")] + public async Task RunExecuteMiddleware_should_call_top_level_error_handler_when_middleware_throws() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + A.CallTo(HandleMethodFor(middleware)).ThrowsAsync(new ApplicationException("Something went wrong")); + Middleware.AddRange(new[] { middleware }); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = + "RunExecuteMiddleware should call error handler on workflow def when middleware throws and def has handler defined")] + public async Task + RunExecuteMiddleware_should_call_error_handler_on_workflow_def_when_middleware_throws_and_def_has_handler() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + A.CallTo(HandleMethodFor(middleware)).ThrowsAsync(new ApplicationException("Something went wrong")); + Middleware.AddRange(new[] { middleware }); + Definition.OnExecuteMiddlewareError = typeof(IDefLevelErrorHandler); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .MustNotHaveHappened(); + A + .CallTo(HandleMethodFor(DefLevelErrorHandler)) + .MustHaveHappenedOnceExactly(); + } + #region Helpers private IWorkflowMiddleware BuildWorkflowMiddleware( From ae7d64f150e84f0755ecbc8cadaa842fcda78a6a Mon Sep 17 00:00:00 2001 From: glucaci Date: Fri, 16 Apr 2021 00:21:58 +0300 Subject: [PATCH 07/11] Add tests --- .../Services/WorkflowMiddlewareRunnerTests.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs b/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs index 0e9c0da07..4b58d965f 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs @@ -274,7 +274,7 @@ public async Task RunExecuteMiddleware_should_run_all_middleware_when_multiple_m Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); // Act - await Runner.RunPostMiddleware(Workflow, Definition); + await Runner.RunExecuteMiddleware(Workflow, Definition); // Assert A @@ -288,8 +288,8 @@ public async Task RunExecuteMiddleware_should_run_all_middleware_when_multiple_m .MustHaveHappenedOnceExactly()); } - [Fact(DisplayName = "RunExecuteMiddleware should run middleware in ExecuteWorkflow and PostWorkflow phase")] - public async Task RunExecuteMiddleware_should_run_middleware_in_ExecuteWorkflow_and_PostWorkflow_phase() + [Fact(DisplayName = "RunExecuteMiddleware should only run middleware in ExecuteWorkflow phase")] + public async Task RunExecuteMiddleware_should_only_run_middleware_in_ExecuteWorkflow_phase() { // Arrange var executeMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); @@ -298,18 +298,15 @@ public async Task RunExecuteMiddleware_should_run_middleware_in_ExecuteWorkflow_ Middleware.AddRange(new[] { preMiddleware, postMiddleware, executeMiddleware }); // Act - // TODO: add same test when workflow not completed await Runner.RunExecuteMiddleware(Workflow, Definition); // Assert A .CallTo(HandleMethodFor(executeMiddleware)) - .MustHaveHappenedOnceExactly() - .Then(A - .CallTo(HandleMethodFor(postMiddleware)) - .MustHaveHappenedOnceExactly()); + .MustHaveHappenedOnceExactly(); A.CallTo(HandleMethodFor(preMiddleware)).MustNotHaveHappened(); + A.CallTo(HandleMethodFor(postMiddleware)).MustNotHaveHappened(); } [Fact(DisplayName = "RunExecuteMiddleware should call top level error handler when middleware throws")] From 0239cd3a76cf0b71f13adb335a3c6f28ccf96b1b Mon Sep 17 00:00:00 2001 From: glucaci Date: Fri, 16 Apr 2021 00:40:35 +0300 Subject: [PATCH 08/11] Add two more tests --- .../Services/WorkflowExecutorFixture.cs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs index 037dd0e33..e548b8e88 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs @@ -44,8 +44,14 @@ public WorkflowExecutorFixture() Options = new WorkflowOptions(A.Fake()); + var stepExecutionScope = A.Fake(); + A.CallTo(() => ScopeProvider.CreateScope(A._)).Returns(stepExecutionScope); + A.CallTo(() => stepExecutionScope.ServiceProvider).Returns(ServiceProvider); + var scope = A.Fake(); - A.CallTo(() => ScopeProvider.CreateScope(A._)).Returns(scope); + var scopeFactory = A.Fake(); + A.CallTo(() => ServiceProvider.GetService(typeof(IServiceScopeFactory))).Returns(scopeFactory); + A.CallTo(() => scopeFactory.CreateScope()).Returns(scope); A.CallTo(() => scope.ServiceProvider).Returns(ServiceProvider); A.CallTo(() => DateTimeProvider.Now).Returns(DateTime.Now); @@ -63,6 +69,10 @@ public WorkflowExecutorFixture() .RunPostMiddleware(A._, A._)) .Returns(Task.CompletedTask); + A.CallTo(() => MiddlewareRunner + .RunExecuteMiddleware(A._, A._)) + .Returns(Task.CompletedTask); + A.CallTo(() => StepExecutor.ExecuteStep(A._, A._)) .ReturnsLazily(call => call.Arguments[1].As().RunAsync( @@ -105,6 +115,64 @@ public void should_execute_active_step() A.CallTo(() => ResultProcesser.ProcessExecutionResult(instance, A.Ignored, A.Ignored, step1, A.Ignored, A.Ignored)).MustHaveHappened(); } + [Fact(DisplayName = "Should call execute middleware when not completed")] + public void should_call_execute_middleware_when_not_completed() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); + + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; + + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => MiddlewareRunner.RunExecuteMiddleware(instance, A.Ignored)).MustHaveHappened(); + } + + [Fact(DisplayName = "Should not call post middleware when not completed")] + public void should_not_call_post_middleware_when_not_completed() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); + + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; + + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => MiddlewareRunner.RunPostMiddleware(instance, A.Ignored)).MustNotHaveHappened(); + } + [Fact(DisplayName = "Should trigger step hooks")] public void should_trigger_step_hooks() { From 2164e0cb273c793fe4a7b05603f401e0707ef310 Mon Sep 17 00:00:00 2001 From: glucaci Date: Fri, 16 Apr 2021 00:47:22 +0300 Subject: [PATCH 09/11] Cleanup --- .../Services/WorkflowMiddlewareRunner.cs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs index 872721849..d341ed1ec 100644 --- a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs +++ b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs @@ -33,33 +33,46 @@ public async Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition } /// - public async Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + public Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) { - var postMiddleware = _middleware - .Where(m => m.Phase == WorkflowMiddlewarePhase.PostWorkflow); - try - { - await RunWorkflowMiddleware(workflow, postMiddleware); - } - catch (Exception exception) - { - await HandleWorkflowMiddlewareError(def.OnPostMiddlewareError, exception); - } + return RunWorkflowMiddlewareWithErrorHandling( + workflow, + WorkflowMiddlewarePhase.PostWorkflow, + def.OnPostMiddlewareError); } /// - public async Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + public Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def) { - var executeMiddleware = _middleware - .Where(m => m.Phase == WorkflowMiddlewarePhase.ExecuteWorkflow); + return RunWorkflowMiddlewareWithErrorHandling( + workflow, + WorkflowMiddlewarePhase.ExecuteWorkflow, + def.OnExecuteMiddlewareError); + } + + public async Task RunWorkflowMiddlewareWithErrorHandling( + WorkflowInstance workflow, + WorkflowMiddlewarePhase phase, + Type middlewareErrorType) + { + var middleware = _middleware.Where(m => m.Phase == phase); try { - await RunWorkflowMiddleware(workflow, executeMiddleware); + await RunWorkflowMiddleware(workflow, middleware); } catch (Exception exception) { - await HandleWorkflowMiddlewareError(def.OnExecuteMiddlewareError, exception); + var errorHandlerType = middlewareErrorType ?? typeof(IWorkflowMiddlewareErrorHandler); + + using (var scope = _serviceProvider.CreateScope()) + { + var typeInstance = scope.ServiceProvider.GetService(errorHandlerType); + if (typeInstance is IWorkflowMiddlewareErrorHandler handler) + { + await handler.HandleAsync(exception); + } + } } } @@ -73,19 +86,5 @@ private static Task RunWorkflowMiddleware( (previous, middleware) => () => middleware.HandleAsync(workflow, previous))(); } - - private async Task HandleWorkflowMiddlewareError(Type middlewareErrorType, Exception exception) - { - var errorHandlerType = middlewareErrorType ?? typeof(IWorkflowMiddlewareErrorHandler); - - using (var scope = _serviceProvider.CreateScope()) - { - var typeInstance = scope.ServiceProvider.GetService(errorHandlerType); - if (typeInstance is IWorkflowMiddlewareErrorHandler handler) - { - await handler.HandleAsync(exception); - } - } - } } } From 8fd8bba630e645b309e3c932c749f9762b25326d Mon Sep 17 00:00:00 2001 From: glucaci Date: Fri, 16 Apr 2021 00:48:31 +0300 Subject: [PATCH 10/11] Cleanup --- src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs index d341ed1ec..d904c5aa5 100644 --- a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs +++ b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs @@ -82,9 +82,9 @@ private static Task RunWorkflowMiddleware( { return middlewareCollection .Reverse() - .Aggregate(NoopWorkflowDelegate, - (previous, middleware) => - () => middleware.HandleAsync(workflow, previous))(); + .Aggregate( + NoopWorkflowDelegate, + (previous, middleware) => () => middleware.HandleAsync(workflow, previous))(); } } } From 045cb49ecaf3a89ded5ee4184c16b70030573306 Mon Sep 17 00:00:00 2001 From: glucaci Date: Sun, 18 Apr 2021 23:19:59 +0300 Subject: [PATCH 11/11] Add release notes and increment minor --- ReleaseNotes/3.4.0.md | 55 ++++++++++++++++++++++++++++ src/WorkflowCore/WorkflowCore.csproj | 8 ++-- 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 ReleaseNotes/3.4.0.md diff --git a/ReleaseNotes/3.4.0.md b/ReleaseNotes/3.4.0.md new file mode 100644 index 000000000..ca724c4b2 --- /dev/null +++ b/ReleaseNotes/3.4.0.md @@ -0,0 +1,55 @@ +# Workflow Core 3.4.0 + +## Execute Workflow Middleware + +These middleware get run after each workflow execution and can be used to perform additional actions or build metrics/statistics for all workflows in your app. + +The following example illustrates how you can use a execute workflow middleware to build [prometheus](https://prometheus.io/) metrics. + +Note that you use `WorkflowMiddlewarePhase.ExecuteWorkflow` to specify that it runs after each workflow execution. + +**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs. + +```cs +public class MetricsMiddleware : IWorkflowMiddleware +{ + private readonly ConcurrentHashSet() _suspendedWorkflows = + new ConcurrentHashSet(); + + private readonly Counter _completed; + private readonly Counter _suspended; + + public MetricsMiddleware() + { + _completed = Prometheus.Metrics.CreateCounter( + "workflow_completed", "Workflow completed"); + + _suspended = Prometheus.Metrics.CreateCounter( + "workflow_suspended", "Workflow suspended"); + } + + public WorkflowMiddlewarePhase Phase => + WorkflowMiddlewarePhase.ExecuteWorkflow; + + public Task HandleAsync( + WorkflowInstance workflow, + WorkflowDelegate next) + { + switch (workflow.Status) + { + case WorkflowStatus.Complete: + if (_suspendedWorkflows.TryRemove(workflow.Id)) + { + _suspended.Dec(); + } + _completed.Inc(); + break; + case WorkflowStatus.Suspended: + _suspended.Inc(); + break; + } + + return next(); + } +} +``` \ No newline at end of file diff --git a/src/WorkflowCore/WorkflowCore.csproj b/src/WorkflowCore/WorkflowCore.csproj index 2ef5a4a1c..a439d3bac 100644 --- a/src/WorkflowCore/WorkflowCore.csproj +++ b/src/WorkflowCore/WorkflowCore.csproj @@ -15,12 +15,12 @@ false false Workflow Core is a light weight workflow engine targeting .NET Standard. - 3.3.6 - 3.3.6.0 - 3.3.6.0 + 3.4.0 + 3.4.0.0 + 3.4.0.0 https://github.com/danielgerlag/workflow-core/raw/master/src/logo.png - 3.3.6 + 3.4.0