diff --git a/WorkflowCore.sln b/WorkflowCore.sln index 047ccf0c3..039bdacc5 100644 --- a/WorkflowCore.sln +++ b/WorkflowCore.sln @@ -141,6 +141,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.DSL", "src\Wor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample18", "src\samples\WorkflowCore.Sample18\WorkflowCore.Sample18.csproj", "{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.Sample19", "src\samples\WorkflowCore.Sample19\WorkflowCore.Sample19.csproj", "{7F8775E5-30AD-415C-B66D-1C76472C77D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -343,6 +345,10 @@ Global {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.Build.0 = Debug|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.Build.0 = Release|Any CPU + {7F8775E5-30AD-415C-B66D-1C76472C77D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F8775E5-30AD-415C-B66D-1C76472C77D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F8775E5-30AD-415C-B66D-1C76472C77D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F8775E5-30AD-415C-B66D-1C76472C77D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -400,6 +406,7 @@ Global {78217204-B873-40B9-8875-E3925B2FBCEC} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} {20B98905-08CB-4854-8E2C-A31A078383E9} = {EF47161E-E399-451C-BDE8-E92AAD3BD761} {5BE6D628-B9DB-4C76-AAEB-8F3800509A84} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} + {7F8775E5-30AD-415C-B66D-1C76472C77D2} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4} diff --git a/src/WorkflowCore/Interface/ICatchStepBuilder.cs b/src/WorkflowCore/Interface/ICatchStepBuilder.cs new file mode 100644 index 000000000..466064008 --- /dev/null +++ b/src/WorkflowCore/Interface/ICatchStepBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace WorkflowCore.Interface +{ + public interface ICatchStepBuilder : IStepBuilder, ITryStepBuilder + where TStepBody : IStepBody + { + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IExecutionPointerFactory.cs b/src/WorkflowCore/Interface/IExecutionPointerFactory.cs index d23ee98fe..77e7f18a4 100644 --- a/src/WorkflowCore/Interface/IExecutionPointerFactory.cs +++ b/src/WorkflowCore/Interface/IExecutionPointerFactory.cs @@ -1,4 +1,5 @@ -using WorkflowCore.Models; +using System; +using WorkflowCore.Models; namespace WorkflowCore.Interface { @@ -6,6 +7,7 @@ public interface IExecutionPointerFactory { ExecutionPointer BuildGenesisPointer(WorkflowDefinition def); ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int compensationStepId); + ExecutionPointer BuildCatchPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int catchStepId, Exception exception); ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, IStepOutcome outcomeTarget); ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPointer pointer, int childDefinitionId, object branch); } diff --git a/src/WorkflowCore/Interface/IStepBuilder.cs b/src/WorkflowCore/Interface/IStepBuilder.cs index db28b5887..4db9f7367 100644 --- a/src/WorkflowCore/Interface/IStepBuilder.cs +++ b/src/WorkflowCore/Interface/IStepBuilder.cs @@ -219,6 +219,8 @@ public interface IStepBuilder /// IStepBuilder Saga(Action> builder); + ITryStepBuilder Try(Action> builder); + /// /// Schedule a block of steps to execute in parallel sometime in the future /// diff --git a/src/WorkflowCore/Interface/IStepExecutionContext.cs b/src/WorkflowCore/Interface/IStepExecutionContext.cs index e59198a13..89247e7c6 100644 --- a/src/WorkflowCore/Interface/IStepExecutionContext.cs +++ b/src/WorkflowCore/Interface/IStepExecutionContext.cs @@ -1,4 +1,5 @@ -using WorkflowCore.Models; +using System; +using WorkflowCore.Models; namespace WorkflowCore.Interface { @@ -12,6 +13,8 @@ public interface IStepExecutionContext WorkflowStep Step { get; set; } - WorkflowInstance Workflow { get; set; } + WorkflowInstance Workflow { get; set; } + + SerializableException CurrentException { get; set; } } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/ITryStepBuilder.cs b/src/WorkflowCore/Interface/ITryStepBuilder.cs new file mode 100644 index 000000000..75541b8ed --- /dev/null +++ b/src/WorkflowCore/Interface/ITryStepBuilder.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface ITryStepBuilder + where TStepBody : IStepBody + { + ICatchStepBuilder Catch(IEnumerable exceptionTypes, Action> stepSetup = null) + where TStep : IStepBody; + + ICatchStepBuilder Catch(IEnumerable exceptionTypes, Action body); + + ICatchStepBuilder Catch(IEnumerable exceptionTypes, + Func body); + + ICatchStepBuilder CatchWithSequence(IEnumerable exceptionTypes, + Action> builder); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IWorkflowDefinitionValidator.cs b/src/WorkflowCore/Interface/IWorkflowDefinitionValidator.cs new file mode 100644 index 000000000..696601ee2 --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowDefinitionValidator.cs @@ -0,0 +1,9 @@ +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IWorkflowDefinitionValidator + { + bool IsDefinitionValid(WorkflowDefinition definition); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Models/ExecutionPointer.cs b/src/WorkflowCore/Models/ExecutionPointer.cs index 53938d76a..3d3640ccc 100644 --- a/src/WorkflowCore/Models/ExecutionPointer.cs +++ b/src/WorkflowCore/Models/ExecutionPointer.cs @@ -45,6 +45,8 @@ public class ExecutionPointer public object Outcome { get; set; } public PointerStatus Status { get; set; } = PointerStatus.Legacy; + + public SerializableException CurrentException { get; set; } public IReadOnlyCollection Scope { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs index 07ff8f75f..5c8c25a07 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { public class WorkflowTerminated : LifeCycleEvent { + public SerializableException Exception { get; set; } } } diff --git a/src/WorkflowCore/Models/SerializableException.cs b/src/WorkflowCore/Models/SerializableException.cs new file mode 100644 index 000000000..3880c6ec2 --- /dev/null +++ b/src/WorkflowCore/Models/SerializableException.cs @@ -0,0 +1,20 @@ +using System; + +namespace WorkflowCore.Models +{ + public class SerializableException + { + public string FullTypeName { get; private set; } + + public string Message { get; private set; } + + public string StackTrace { get; private set; } + + public SerializableException(Exception exception) + { + FullTypeName = exception.GetType().FullName; + Message = exception.Message; + StackTrace = exception.StackTrace; + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Models/StepExecutionContext.cs b/src/WorkflowCore/Models/StepExecutionContext.cs index b48bcd35c..6bf7bfdd9 100644 --- a/src/WorkflowCore/Models/StepExecutionContext.cs +++ b/src/WorkflowCore/Models/StepExecutionContext.cs @@ -1,4 +1,5 @@ -using WorkflowCore.Interface; +using System; +using WorkflowCore.Interface; namespace WorkflowCore.Models { @@ -13,5 +14,7 @@ public class StepExecutionContext : IStepExecutionContext public object PersistenceData { get; set; } public object Item { get; set; } + + public SerializableException CurrentException { get; set; } } } diff --git a/src/WorkflowCore/Models/WorkflowDefinition.cs b/src/WorkflowCore/Models/WorkflowDefinition.cs index f39a563ed..ac1e73ff5 100644 --- a/src/WorkflowCore/Models/WorkflowDefinition.cs +++ b/src/WorkflowCore/Models/WorkflowDefinition.cs @@ -27,6 +27,7 @@ public enum WorkflowErrorHandling Retry = 0, Suspend = 1, Terminate = 2, - Compensate = 3 + Compensate = 3, + Catch = 4 } } diff --git a/src/WorkflowCore/Models/WorkflowStep.cs b/src/WorkflowCore/Models/WorkflowStep.cs index 08ad1c525..244fcec30 100644 --- a/src/WorkflowCore/Models/WorkflowStep.cs +++ b/src/WorkflowCore/Models/WorkflowStep.cs @@ -26,9 +26,11 @@ public abstract class WorkflowStep public virtual WorkflowErrorHandling? ErrorBehavior { get; set; } - public virtual TimeSpan? RetryInterval { get; set; } + public virtual TimeSpan? RetryInterval { get; set; } public virtual int? CompensationStepId { get; set; } + + public virtual Queue> CatchStepsQueue { get; set; } = new Queue>(); public virtual bool ResumeChildrenAfterCompensation => true; diff --git a/src/WorkflowCore/ServiceCollectionExtensions.cs b/src/WorkflowCore/ServiceCollectionExtensions.cs index c241107c2..be183f92b 100644 --- a/src/WorkflowCore/ServiceCollectionExtensions.cs +++ b/src/WorkflowCore/ServiceCollectionExtensions.cs @@ -47,6 +47,7 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); @@ -58,6 +59,7 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient, InjectedObjectPoolPolicy>(); services.AddTransient, InjectedObjectPoolPolicy>(); diff --git a/src/WorkflowCore/Services/ErrorHandlers/CatchHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/CatchHandler.cs new file mode 100644 index 000000000..823430be5 --- /dev/null +++ b/src/WorkflowCore/Services/ErrorHandlers/CatchHandler.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Services.ErrorHandlers +{ + public class CatchHandler : IWorkflowErrorHandler + { + private readonly ILifeCycleEventPublisher _eventPublisher; + private readonly IExecutionPointerFactory _pointerFactory; + private readonly IDateTimeProvider _datetimeProvider; + private readonly WorkflowOptions _options; + + public CatchHandler(IExecutionPointerFactory pointerFactory, ILifeCycleEventPublisher eventPublisher, IDateTimeProvider datetimeProvider, WorkflowOptions options) + { + _pointerFactory = pointerFactory; + _eventPublisher = eventPublisher; + _datetimeProvider = datetimeProvider; + _options = options; + } + + public WorkflowErrorHandling Type => WorkflowErrorHandling.Catch; + + public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer exceptionPointer, WorkflowStep step, + Exception exception, Queue bubbleUpQueue) + { + var scope = new Stack(exceptionPointer.Scope.Reverse()); + scope.Push(exceptionPointer.Id); + + var exceptionCaught = false; + + while (scope.Any()) + { + var pointerId = scope.Pop(); + var scopePointer = workflow.ExecutionPointers.FindById(pointerId); + var scopeStep = def.Steps.FindById(scopePointer.StepId); + + if ((scopeStep.ErrorBehavior ?? WorkflowErrorHandling.Catch) != WorkflowErrorHandling.Catch) + { + bubbleUpQueue.Enqueue(scopePointer); + continue; + } //TODO: research if it's needed + + scopePointer.Active = false; + scopePointer.EndTime = _datetimeProvider.Now.ToUniversalTime(); + scopePointer.Status = PointerStatus.Failed; + + while (scopeStep.CatchStepsQueue.Count != 0) + { + var nextCatchStepPair = scopeStep.CatchStepsQueue.Dequeue(); + var exceptionType = nextCatchStepPair.Key; + var catchStepId = nextCatchStepPair.Value; + if (exceptionType.IsInstanceOfType(exception)) + { + var catchPointer = _pointerFactory.BuildCatchPointer(def, scopePointer, exceptionPointer, catchStepId, exception); + workflow.ExecutionPointers.Add(catchPointer); + + foreach (var outcomeTarget in scopeStep.Outcomes.Where(x => x.Matches(workflow.Data))) + workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, scopePointer, outcomeTarget)); + + exceptionCaught = true; + + scopeStep.CatchStepsQueue.Clear(); + scope.Clear(); + break; + } + } + } + + if (!exceptionCaught) + { + workflow.Status = WorkflowStatus.Terminated; + _eventPublisher.PublishNotification(new WorkflowTerminated() + { + EventTimeUtc = _datetimeProvider.UtcNow, + Reference = workflow.Reference, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version, + Exception = new SerializableException(exception) + }); + } + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs index d89f7f470..18cc4dba1 100755 --- a/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs +++ b/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs @@ -28,7 +28,8 @@ public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionP Reference = workflow.Reference, WorkflowInstanceId = workflow.Id, WorkflowDefinitionId = workflow.WorkflowDefinitionId, - Version = workflow.Version + Version = workflow.Version, + Exception = new SerializableException(exception) }); } } diff --git a/src/WorkflowCore/Services/ExecutionPointerFactory.cs b/src/WorkflowCore/Services/ExecutionPointerFactory.cs index 65f5a2f8f..ff9a0ffcd 100644 --- a/src/WorkflowCore/Services/ExecutionPointerFactory.cs +++ b/src/WorkflowCore/Services/ExecutionPointerFactory.cs @@ -72,6 +72,23 @@ public ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, Executi Scope = new List(pointer.Scope) }; } + + public ExecutionPointer BuildCatchPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int catchStepId, Exception exception) + { + var nextId = GenerateId(); + return new ExecutionPointer() + { + Id = nextId, + PredecessorId = exceptionPointer.Id, + StepId = catchStepId, + Active = true, + ContextItem = pointer.ContextItem, + Status = PointerStatus.Pending, + StepName = def.Steps.FindById(catchStepId).Name, + Scope = new List(pointer.Scope), + CurrentException = new SerializableException(exception) + }; + } private string GenerateId() { diff --git a/src/WorkflowCore/Services/ExecutionResultProcessor.cs b/src/WorkflowCore/Services/ExecutionResultProcessor.cs index 448506823..b42a3062a 100755 --- a/src/WorkflowCore/Services/ExecutionResultProcessor.cs +++ b/src/WorkflowCore/Services/ExecutionResultProcessor.cs @@ -112,8 +112,7 @@ public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition de { var exceptionPointer = queue.Dequeue(); var exceptionStep = def.Steps.FindById(exceptionPointer.StepId); - var shouldCompensate = ShouldCompensate(workflow, def, exceptionPointer); - var errorOption = (exceptionStep.ErrorBehavior ?? (shouldCompensate ? WorkflowErrorHandling.Compensate : def.DefaultErrorBehavior)); + var errorOption = (exceptionStep.ErrorBehavior ?? GetErrorHandling(workflow, def, exceptionPointer)); foreach (var handler in _errorHandlers.Where(x => x.Type == errorOption)) { @@ -121,8 +120,9 @@ public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition de } } } - - private bool ShouldCompensate(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer currentPointer) + + private WorkflowErrorHandling GetErrorHandling(WorkflowInstance workflow, WorkflowDefinition def, + ExecutionPointer currentPointer) { var scope = new Stack(currentPointer.Scope); scope.Push(currentPointer.Id); @@ -132,11 +132,13 @@ private bool ShouldCompensate(WorkflowInstance workflow, WorkflowDefinition def, var pointerId = scope.Pop(); var pointer = workflow.ExecutionPointers.FindById(pointerId); var step = def.Steps.FindById(pointer.StepId); + if (step.CatchStepsQueue.Count != 0) + return WorkflowErrorHandling.Catch; if ((step.CompensationStepId.HasValue) || (step.RevertChildrenAfterCompensation)) - return true; + return WorkflowErrorHandling.Compensate; } - return false; + return def.DefaultErrorBehavior; } } } \ No newline at end of file diff --git a/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs index 93f965eba..9e5ee7b52 100644 --- a/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs +++ b/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq.Expressions; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -7,7 +8,9 @@ namespace WorkflowCore.Services { - public class StepBuilder : IStepBuilder, IContainerStepBuilder + public class StepBuilder + : IContainerStepBuilder, + ICatchStepBuilder where TStepBody : IStepBody { public IWorkflowBuilder WorkflowBuilder { get; private set; } @@ -379,6 +382,78 @@ public IStepBuilder Saga(Action> builde return stepBuilder; } + public ITryStepBuilder Try(Action> builder) + { + var newStep = new WorkflowStep(); + newStep.ErrorBehavior = WorkflowErrorHandling.Catch; + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + builder.Invoke(WorkflowBuilder); + stepBuilder.Step.Children.Add(stepBuilder.Step.Id + 1); //TODO: make more elegant + + return stepBuilder; + } + + public ICatchStepBuilder Catch(IEnumerable exceptionTypes, Action> stepSetup = null) + where TStep : IStepBody + { + var newStep = new WorkflowStep(); + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + + stepSetup?.Invoke(stepBuilder); + + newStep.Name = newStep.Name ?? typeof(TStep).Name; + + foreach (var exceptionType in exceptionTypes) + { + Step.CatchStepsQueue.Enqueue(new KeyValuePair(exceptionType, newStep.Id)); + } + + return this; + } + + public ICatchStepBuilder Catch(IEnumerable exceptionTypes, Func body) + { + WorkflowStepInline newStep = new WorkflowStepInline(); + newStep.Body = body; + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + foreach (var exceptionType in exceptionTypes) + { + Step.CatchStepsQueue.Enqueue(new KeyValuePair(exceptionType, newStep.Id)); + } + return this; + } + + public ICatchStepBuilder Catch(IEnumerable exceptionTypes, Action body) + { + var newStep = new WorkflowStep(); + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + stepBuilder.Input(x => x.Body, x => body); + foreach (var exceptionType in exceptionTypes) + { + Step.CatchStepsQueue.Enqueue(new KeyValuePair(exceptionType, newStep.Id)); + } + return this; + } + + public ICatchStepBuilder CatchWithSequence(IEnumerable exceptionTypes, Action> builder) + { + var newStep = new WorkflowStep(); + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + builder.Invoke(WorkflowBuilder); + stepBuilder.Step.Children.Add(stepBuilder.Step.Id + 1); //TODO: make more elegant + foreach (var exceptionType in exceptionTypes) + { + Step.CatchStepsQueue.Enqueue(new KeyValuePair(exceptionType, newStep.Id)); + } + return this; + } + public IParallelStepBuilder Parallel() { var newStep = new WorkflowStep(); diff --git a/src/WorkflowCore/Services/WorkflowDefinitionValidator.cs b/src/WorkflowCore/Services/WorkflowDefinitionValidator.cs new file mode 100644 index 000000000..3320eb335 --- /dev/null +++ b/src/WorkflowCore/Services/WorkflowDefinitionValidator.cs @@ -0,0 +1,15 @@ +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + public class WorkflowDefinitionValidator : IWorkflowDefinitionValidator + { + public bool IsDefinitionValid(WorkflowDefinition definition) + { + return definition.Steps.Count(x => x.ErrorBehavior == WorkflowErrorHandling.Catch && + x.CatchStepsQueue.Count == 0) == 0; + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs index 7597e7490..a399e5de9 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -163,7 +163,8 @@ private async Task ExecuteStep(WorkflowInstance workflow, WorkflowStep step, Exe Step = step, PersistenceData = pointer.PersistenceData, ExecutionPointer = pointer, - Item = pointer.ContextItem + Item = pointer.ContextItem, + CurrentException = pointer.CurrentException }; foreach (var input in step.Inputs) diff --git a/src/WorkflowCore/Services/WorkflowRegistry.cs b/src/WorkflowCore/Services/WorkflowRegistry.cs index 8ed6a6f7b..1bebe3d46 100644 --- a/src/WorkflowCore/Services/WorkflowRegistry.cs +++ b/src/WorkflowCore/Services/WorkflowRegistry.cs @@ -11,10 +11,12 @@ public class WorkflowRegistry : IWorkflowRegistry { private readonly IServiceProvider _serviceProvider; private readonly List> _registry = new List>(); + private readonly IWorkflowDefinitionValidator _validator; - public WorkflowRegistry(IServiceProvider serviceProvider) + public WorkflowRegistry(IServiceProvider serviceProvider, IWorkflowDefinitionValidator validator) { _serviceProvider = serviceProvider; + _validator = validator; } public WorkflowDefinition GetDefinition(string workflowId, int? version = null) @@ -52,6 +54,12 @@ public void RegisterWorkflow(IWorkflow workflow) var builder = _serviceProvider.GetService().UseData(); workflow.Build(builder); var def = builder.Build(workflow.Id, workflow.Version); + + if (!_validator.IsDefinitionValid(def)) + { + throw new InvalidOperationException($"Workflow {workflow.Id} version {workflow.Version} is not valid"); + } + _registry.Add(Tuple.Create(workflow.Id, workflow.Version, def)); } @@ -61,6 +69,11 @@ public void RegisterWorkflow(WorkflowDefinition definition) { throw new InvalidOperationException($"Workflow {definition.Id} version {definition.Version} is already registered"); } + + if (!_validator.IsDefinitionValid(definition)) + { + throw new InvalidOperationException($"Workflow {definition.Id} version {definition.Version} is not valid"); + } _registry.Add(Tuple.Create(definition.Id, definition.Version, definition)); } @@ -76,6 +89,12 @@ public void RegisterWorkflow(IWorkflow workflow) var builder = _serviceProvider.GetService().UseData(); workflow.Build(builder); var def = builder.Build(workflow.Id, workflow.Version); + + if (!_validator.IsDefinitionValid(def)) + { + throw new InvalidOperationException($"Workflow {workflow.Id} version {workflow.Version} is not valid"); + } + _registry.Add(Tuple.Create(workflow.Id, workflow.Version, def)); } diff --git a/src/samples/WorkflowCore.Sample19/CustomMessage.cs b/src/samples/WorkflowCore.Sample19/CustomMessage.cs new file mode 100644 index 000000000..a7572ceaa --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/CustomMessage.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19 +{ + public class CustomMessage : StepBody + { + + public string Message { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine(Message); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Program.cs b/src/samples/WorkflowCore.Sample19/Program.cs new file mode 100644 index 000000000..f5399bbc7 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Program.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.Interface; + +namespace WorkflowCore.Sample19 +{ + class Program + { + static void Main(string[] args) + { + var serviceProvider = ConfigureServices(); + + //start the workflow host + var host = serviceProvider.GetService(); + host.RegisterWorkflow(); + host.Start(); + + Console.WriteLine("Starting workflow..."); + var workflowId = host.StartWorkflow("try-catch-sample").Result; + + Console.ReadLine(); + host.Stop(); + } + + private static IServiceProvider ConfigureServices() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + services.AddWorkflow(); + //services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow")); + //services.AddWorkflow(x => x.UseSqlServer(@"Server=.;Database=WorkflowCore;Trusted_Connection=True;", true, true)); + //services.AddWorkflow(x => x.UsePostgreSQL(@"Server=127.0.0.1;Port=5432;Database=workflow;User Id=postgres;", true, true)); + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + } +} \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample19/TryCatchWorkflow.cs b/src/samples/WorkflowCore.Sample19/TryCatchWorkflow.cs new file mode 100644 index 000000000..15c4432be --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/TryCatchWorkflow.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19 +{ + class TryCatchWorkflow : IWorkflow + { + public string Id => "try-catch-sample"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder.StartWith(_ => ExecutionResult.Next()) + .Output(data => data.Message, step => "Custom Message") + .Try(b => b.StartWith(_ => ExecutionResult.Next()) + .Try(b2 => b2.StartWith(_ => throw new Exception("I am Exception1"))) + .Catch(new []{typeof(ApplicationException)}, ctx => + { + Console.WriteLine( + $"Caught an exception in inner catch: Type: '{ctx.CurrentException.FullTypeName}', Message: '{ctx.CurrentException.Message}'"); + return ExecutionResult.Next(); + })) + .Catch(new []{typeof(Exception)}, ctx => + { + Console.WriteLine( + $"Caught an exception in outer catch: Type: '{ctx.CurrentException.FullTypeName}', Message: '{ctx.CurrentException.Message}'"); + return ExecutionResult.Next(); + }) + .Then(s => s.Input(msg => msg.Message, data => data.Message)); + } + + public class Data + { + public string Message { get; set; } + } + } +} \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample19/WorkflowCore.Sample19.csproj b/src/samples/WorkflowCore.Sample19/WorkflowCore.Sample19.csproj new file mode 100644 index 000000000..c843999c5 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/WorkflowCore.Sample19.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/TryCatchScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/TryCatchScenario.cs new file mode 100644 index 000000000..bb4fc619e --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/TryCatchScenario.cs @@ -0,0 +1,79 @@ +using System; +using FluentAssertions; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Testing; +using Xunit; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class TryCatchScenario: WorkflowTest + { + public class MyDataClass + { + public bool ThrowException { get; set; } + } + + public class Workflow : IWorkflow + { + public static bool Event1Fired = false; + public static bool Event2Fired = false; + public static bool TailEventFired = false; + public static bool CatchFired = false; + + public string Id => "TryCatchWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Try(b => b.StartWith(ctx => { + Event1Fired = true; + if (((MyDataClass) ctx.Workflow.Data).ThrowException) + throw new Exception(); + Event2Fired = true; + }) + ) + .Catch(new[]{typeof(Exception)}, context => CatchFired = true) + .Then(context => TailEventFired = true); + } + } + + public TryCatchScenario() + { + Setup(); + Workflow.Event1Fired = false; + Workflow.Event2Fired = false; + Workflow.CatchFired = false; + Workflow.TailEventFired = false; + } + + [Fact] + public void NoExceptionScenario() + { + var workflowId = StartWorkflow(new MyDataClass() { ThrowException = false }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + Workflow.Event1Fired.Should().BeTrue(); + Workflow.Event2Fired.Should().BeTrue(); + Workflow.CatchFired.Should().BeFalse(); + Workflow.TailEventFired.Should().BeTrue(); + } + + [Fact] + public void ExceptionScenario() + { + var workflowId = StartWorkflow(new MyDataClass() { ThrowException = true }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(1); + Workflow.Event1Fired.Should().BeTrue(); + Workflow.Event2Fired.Should().BeFalse(); + Workflow.CatchFired.Should().BeTrue(); + Workflow.TailEventFired.Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.UnitTests/Handlers/CatchHandlerFixture.cs b/test/WorkflowCore.UnitTests/Handlers/CatchHandlerFixture.cs new file mode 100644 index 000000000..a717f2721 --- /dev/null +++ b/test/WorkflowCore.UnitTests/Handlers/CatchHandlerFixture.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; +using WorkflowCore.Primitives; +using WorkflowCore.Services.ErrorHandlers; +using Xunit; + +namespace WorkflowCore.UnitTests.Handlers +{ + public class CatchHandlerFixture + { + private ILifeCycleEventPublisher _eventPublisher; + private IExecutionPointerFactory _pointerFactory; + private IDateTimeProvider _datetimeProvider; + private WorkflowOptions _options; + private DateTime _now = DateTime.Now; + + private CatchHandler _subject; + + public CatchHandlerFixture() + { + _eventPublisher = A.Fake(); + _pointerFactory = A.Fake(); + _datetimeProvider = A.Fake(); + A.CallTo(() => _datetimeProvider.Now).Returns(_now); + + _options = new WorkflowOptions(A.Fake()); + _subject = new CatchHandler(_pointerFactory, _eventPublisher, _datetimeProvider, _options); + } + + [Fact(DisplayName = "Should have type of Catch")] + public void should_have_type_of_catch() + { + _subject.Type.Should().Be(WorkflowErrorHandling.Catch); + } + + [Fact(DisplayName = "Should catch exception with one catch step")] + public void should_catch_exception_with_one_catch_step() + { + //arrange + var tryStepId = 123; + var tryPointer = SetupTryPointer(tryStepId); + var workflow = SetupWorkflow(new List {tryPointer}); + var tryStep = SetupTryStep(tryStepId); + + var catchStepId = 345; + tryStep.CatchStepsQueue.Enqueue(new KeyValuePair(typeof(Exception), catchStepId)); + + var definition = SetupWorkflowDefinition(tryStep); + + var exception = new ArgumentException("message"); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .Returns(new ExecutionPointer {Id = "catchPointerId"}); + + //act + _subject.Handle(workflow, definition, tryPointer, new WorkflowStepInline(), exception, new Queue()); + + //assert + tryPointer.Active.Should().BeFalse(); + tryPointer.EndTime.Should().Be(_now.ToUniversalTime()); + tryPointer.Status.Should().Be(PointerStatus.Failed); + + workflow.ExecutionPointers.Count.Should().Be(2); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _pointerFactory.BuildNextPointer(A._, A._, + A._)).MustNotHaveHappened(); + + workflow.Status.Should().NotBe(WorkflowStatus.Terminated); + A.CallTo(() => _eventPublisher.PublishNotification(A._)) + .MustNotHaveHappened(); + } + + [Fact(DisplayName = "Should catch exception with inner catch step")] + public void should_catch_exception_with_inner_catch_step() + { + //arrange + var tryStepId = 123; + var tryPointer = SetupTryPointer(tryStepId); + var workflow = SetupWorkflow(new List {tryPointer}); + var tryStep = SetupTryStep(tryStepId); + + var innerCatchStepId = 345; + var outerCatchStepId = 346; + tryStep.CatchStepsQueue.Enqueue(new KeyValuePair(typeof(ArgumentException), innerCatchStepId)); + tryStep.CatchStepsQueue.Enqueue(new KeyValuePair(typeof(Exception), outerCatchStepId)); + + var definition = SetupWorkflowDefinition(tryStep); + + var exception = new ArgumentException("message"); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .ReturnsLazily(call => new ExecutionPointer{Id = call.Arguments[3].ToString()}); + + //act + _subject.Handle(workflow, definition, tryPointer, new WorkflowStepInline(), exception, new Queue()); + + //assert + tryPointer.Active.Should().BeFalse(); + tryPointer.EndTime.Should().Be(_now.ToUniversalTime()); + tryPointer.Status.Should().Be(PointerStatus.Failed); + + workflow.ExecutionPointers.Count.Should().Be(2); + workflow.ExecutionPointers.FindById(innerCatchStepId.ToString()).Should().NotBeNull(); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _pointerFactory.BuildNextPointer(A._, A._, + A._)).MustNotHaveHappened(); + + workflow.Status.Should().NotBe(WorkflowStatus.Terminated); + A.CallTo(() => _eventPublisher.PublishNotification(A._)) + .MustNotHaveHappened(); + } + + [Fact(DisplayName = "Should catch exception with outer catch step")] + public void should_catch_exception_with_outer_catch_step() + { + //arrange + var tryStepId = 123; + var tryPointer = SetupTryPointer(tryStepId); + var workflow = SetupWorkflow(new List {tryPointer}); + var tryStep = SetupTryStep(tryStepId); + + var innerCatchStepId = 345; + var outerCatchStepId = 346; + tryStep.CatchStepsQueue.Enqueue(new KeyValuePair(typeof(ArgumentException), innerCatchStepId)); + tryStep.CatchStepsQueue.Enqueue(new KeyValuePair(typeof(Exception), outerCatchStepId)); + + var definition = SetupWorkflowDefinition(tryStep); + + var exception = new Exception("message"); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .ReturnsLazily(call => new ExecutionPointer{Id = call.Arguments[3].ToString()}); + + //act + _subject.Handle(workflow, definition, tryPointer, new WorkflowStepInline(), exception, new Queue()); + + //assert + tryPointer.Active.Should().BeFalse(); + tryPointer.EndTime.Should().Be(_now.ToUniversalTime()); + tryPointer.Status.Should().Be(PointerStatus.Failed); + + workflow.ExecutionPointers.Count.Should().Be(2); + workflow.ExecutionPointers.FindById(outerCatchStepId.ToString()).Should().NotBeNull(); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _pointerFactory.BuildNextPointer(A._, A._, + A._)).MustNotHaveHappened(); + + workflow.Status.Should().NotBe(WorkflowStatus.Terminated); + A.CallTo(() => _eventPublisher.PublishNotification(A._)) + .MustNotHaveHappened(); + } + + [Fact(DisplayName = "Should not catch exception because of wrong exception type")] + public void should_not_catch_exception_because_of_wrong_exception_type() + { + //arrange + var tryStepId = 123; + var tryPointer = SetupTryPointer(tryStepId); + var workflow = SetupWorkflow(new List {tryPointer}); + var tryStep = SetupTryStep(tryStepId); + + var catchStepId = 345; + tryStep.CatchStepsQueue.Enqueue(new KeyValuePair(typeof(ArgumentException), catchStepId)); + + var definition = SetupWorkflowDefinition(tryStep); + + var exception = new Exception("message"); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .Returns(new ExecutionPointer {Id = "catchPointerId"}); + + //act + _subject.Handle(workflow, definition, tryPointer, new WorkflowStepInline(), exception, new Queue()); + + //assert + tryPointer.Active.Should().BeFalse(); + tryPointer.EndTime.Should().Be(_now.ToUniversalTime()); + tryPointer.Status.Should().Be(PointerStatus.Failed); + + workflow.ExecutionPointers.Count.Should().Be(1); + + A.CallTo(() => _pointerFactory.BuildCatchPointer(A._, A._, + A._, A._, A._)) + .MustNotHaveHappened(); + A.CallTo(() => _pointerFactory.BuildNextPointer(A._, A._, + A._)).MustNotHaveHappened(); + + workflow.Status.Should().Be(WorkflowStatus.Terminated); + A.CallTo(() => _eventPublisher.PublishNotification(A._)) + .MustHaveHappenedOnceExactly(); + } + + private static ExecutionPointer SetupTryPointer(int tryStepId) + { + var tryPointerId = "tryPointerId"; + var tryPointer = new ExecutionPointer + { + Scope = new List(), + Id = tryPointerId, + StepId = tryStepId, + Active = true, + Status = PointerStatus.Pending + }; + return tryPointer; + } + + private static WorkflowInstance SetupWorkflow(ICollection pointers) + { + return new WorkflowInstance + { + ExecutionPointers = new ExecutionPointerCollection(pointers), + Status = WorkflowStatus.Runnable + }; + } + + private static WorkflowStep SetupTryStep(int tryStepId) + { + return new WorkflowStep + { + Id = tryStepId, + ErrorBehavior = WorkflowErrorHandling.Catch + }; + } + + private static WorkflowDefinition SetupWorkflowDefinition(WorkflowStep tryStep) + { + var definition = new WorkflowDefinition(); + definition.Steps.Add(tryStep); + return definition; + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs b/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs index c3dbb96fd..5ec0dd04d 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs @@ -1,30 +1,26 @@ using FakeItEasy; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Services; using FluentAssertions; using Xunit; -using WorkflowCore.Primitives; -using System.Linq.Expressions; -using System.Threading.Tasks; namespace WorkflowCore.UnitTests.Services { public class WorkflowRegistryFixture { protected IServiceProvider ServiceProvider { get; } + protected IWorkflowDefinitionValidator Validator { get; } protected WorkflowRegistry Subject { get; } protected WorkflowDefinition Definition { get; } public WorkflowRegistryFixture() { ServiceProvider = A.Fake(); - Subject = new WorkflowRegistry(ServiceProvider); + Validator = A.Fake(); + A.CallTo(() => Validator.IsDefinitionValid(A.Ignored)).Returns(true); + Subject = new WorkflowRegistry(ServiceProvider, Validator); Definition = new WorkflowDefinition{ Id = "TestWorkflow",