diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..2cf348570b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset=utf-8 +end_of_line=crlf +trim_trailing_whitespace=false +insert_final_newline=false +indent_style=space +indent_size=4 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..35a49b292b --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +#Ignore thumbnails created by Windows +Thumbs.db + +#Ignore files built by Visual Studio +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +.vs/ + +#Nuget packages folder +packages/ + +#Ignore git-related files +*.orig + +#Rider +.idea \ No newline at end of file diff --git a/Flowsharp.sln b/Flowsharp.sln new file mode 100644 index 0000000000..b6d2ff0f8d --- /dev/null +++ b/Flowsharp.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flowsharp.Abstractions", "src\Flowsharp.Abstractions\Flowsharp.Abstractions.csproj", "{300EE2D5-54C5-46F2-AD03-BB43589EA074}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flowsharp.Core", "src\Flowsharp.Core\Flowsharp.Core.csproj", "{3B33AE9C-0465-4DA3-8C02-65E5766A7ED4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flowsharp.Samples.Console", "src\Flowsharp.Samples.Console\Flowsharp.Samples.Console.csproj", "{48EDB976-3227-4DFD-BBB3-3BC9AEA19A88}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{7165BB9E-F22C-40D2-B7B1-CA3EFA2529A0}" +ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + README.md = README.md +EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flowsharp.Fluid", "src\Flowsharp.Fluid\Flowsharp.Fluid.csproj", "{B20D6AF5-91B1-4455-8541-22C5B31288D0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {300EE2D5-54C5-46F2-AD03-BB43589EA074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {300EE2D5-54C5-46F2-AD03-BB43589EA074}.Debug|Any CPU.Build.0 = Debug|Any CPU + {300EE2D5-54C5-46F2-AD03-BB43589EA074}.Release|Any CPU.ActiveCfg = Release|Any CPU + {300EE2D5-54C5-46F2-AD03-BB43589EA074}.Release|Any CPU.Build.0 = Release|Any CPU + {3B33AE9C-0465-4DA3-8C02-65E5766A7ED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B33AE9C-0465-4DA3-8C02-65E5766A7ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B33AE9C-0465-4DA3-8C02-65E5766A7ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B33AE9C-0465-4DA3-8C02-65E5766A7ED4}.Release|Any CPU.Build.0 = Release|Any CPU + {48EDB976-3227-4DFD-BBB3-3BC9AEA19A88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48EDB976-3227-4DFD-BBB3-3BC9AEA19A88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48EDB976-3227-4DFD-BBB3-3BC9AEA19A88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48EDB976-3227-4DFD-BBB3-3BC9AEA19A88}.Release|Any CPU.Build.0 = Release|Any CPU + {B20D6AF5-91B1-4455-8541-22C5B31288D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B20D6AF5-91B1-4455-8541-22C5B31288D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B20D6AF5-91B1-4455-8541-22C5B31288D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B20D6AF5-91B1-4455-8541-22C5B31288D0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {300EE2D5-54C5-46F2-AD03-BB43589EA074} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925} + {3B33AE9C-0465-4DA3-8C02-65E5766A7ED4} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925} + {48EDB976-3227-4DFD-BBB3-3BC9AEA19A88} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925} + {B20D6AF5-91B1-4455-8541-22C5B31288D0} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925} + EndGlobalSection +EndGlobal diff --git a/src/Flowsharp.Abstractions/Activities/Activity.cs b/src/Flowsharp.Abstractions/Activities/Activity.cs new file mode 100644 index 0000000000..045cbc21a9 --- /dev/null +++ b/src/Flowsharp.Abstractions/Activities/Activity.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.ActivityResults; +using Flowsharp.Models; +using Microsoft.Extensions.Localization; + +namespace Flowsharp.Activities +{ + public abstract class Activity : IActivity + { + public virtual string Name => GetType().Name; + + public Task ProvideMetadataAsync(ActivityMetadataContext context, CancellationToken cancellationToken) + { + ProvideMetadata(context); + return Task.CompletedTask; + } + + public virtual Task CanExecuteAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public virtual Task ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken) + { + return Task.FromResult(Execute(workflowContext, activityContext)); + } + + public virtual Task ResumeAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken) + { + return Task.FromResult(Resume(workflowContext, activityContext)); + } + + public virtual IEnumerable GetOutcomes(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext) + { + return Enumerable.Empty(); + } + + public virtual Task OnActivityExecutedAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken) + { + OnActivityExecuted(workflowContext, activityContext); + return Task.CompletedTask; + } + + public virtual Task OnActivityExecutingAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken) + { + OnActivityExecuting(workflowContext, activityContext); + return Task.CompletedTask; + } + + public virtual Task ReceiveInputAsync(WorkflowExecutionContext workflowContext, IDictionary input, CancellationToken cancellationToken) + { + ReceiveInput(workflowContext, input); + return Task.CompletedTask; + } + + public virtual Task WorkflowResumedAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + WorkflowResumed(workflowContext); + return Task.CompletedTask; + } + + public virtual Task WorkflowResumingAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + WorkflowResuming(workflowContext); + return Task.CompletedTask; + } + + public virtual Task WorkflowStartedAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + WorkflowStarted(workflowContext); + return Task.CompletedTask; + } + + public virtual Task WorkflowStartingAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + WorkflowStarting(workflowContext); + return Task.CompletedTask; + } + + protected virtual void ProvideMetadata(ActivityMetadataContext context) + { + } + + protected virtual ActivityExecutionResult Execute(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext) + { + return Noop(); + } + + protected virtual ActivityExecutionResult Resume(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext) + { + return Noop(); + } + + protected virtual void OnActivityExecuted(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext) {} + protected virtual void OnActivityExecuting(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext) {} + protected virtual void ReceiveInput(WorkflowExecutionContext workflowContext, IDictionary input) {} + protected virtual void WorkflowResumed(WorkflowExecutionContext workflowContext) {} + protected virtual void WorkflowResuming(WorkflowExecutionContext workflowContext) {} + protected virtual void WorkflowStarted(WorkflowExecutionContext workflowContext) {} + protected virtual void WorkflowStarting(WorkflowExecutionContext workflowContext) {} + + protected IEnumerable Outcomes(params LocalizedString[] names) + { + return names.Select(x => new Outcome(x)); + } + + protected IEnumerable Outcomes(IEnumerable names) + { + return names.Select(x => new Outcome(x)); + } + + protected ActivityExecutionResult Outcomes(params string[] names) + { + return Outcomes((IEnumerable)names); + } + + protected ActivityExecutionResult Outcomes(IEnumerable names) + { + return new OutcomeResult(names); + } + + protected ActivityExecutionResult Halt() + { + return new HaltResult(); + } + + protected ActivityExecutionResult Noop() + { + return new NoopResult(); + } + } +} diff --git a/src/Flowsharp.Abstractions/Activities/IActivity.cs b/src/Flowsharp.Abstractions/Activities/IActivity.cs new file mode 100644 index 0000000000..f886457213 --- /dev/null +++ b/src/Flowsharp.Abstractions/Activities/IActivity.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.ActivityResults; +using Flowsharp.Models; + +namespace Flowsharp.Activities +{ + public interface IActivity + { + /// + /// The system name of the activity. + /// + /// The Name is used to identify a given activity type and must be unique. + string Name { get; } + + /// + /// Provides metadata about the specified activity. + /// + Task ProvideMetadataAsync(ActivityMetadataContext context, CancellationToken cancellationToken); + + /// + /// Returns a list of possible outcomes when the activity is executed. + /// + IEnumerable GetOutcomes(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext); + + /// + /// Returns a value of whether the specified activity can execute. + /// + Task CanExecuteAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken); + + /// + /// Executes the specified activity. + /// + Task ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken); + + /// + /// Resumes the specified activity. + /// + Task ResumeAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken); + + /// + /// Executes before a workflow starts or resumes, giving activities an opportunity to read and store any values of interest. + /// + Task ReceiveInputAsync(WorkflowExecutionContext workflowContext, IDictionary input, CancellationToken cancellationToken); + + /// + /// Executes when a workflow is about to start. + /// + Task WorkflowStartingAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken); + + /// + /// Executes when a workflow has started. + /// + Task WorkflowStartedAsync(WorkflowExecutionContext context, CancellationToken cancellationToken); + + /// + /// Executes when a workflow is about to be resumed. + /// + Task WorkflowResumingAsync(WorkflowExecutionContext context, CancellationToken cancellationToken); + + /// + /// Executes when a workflow is resumed. + /// + Task WorkflowResumedAsync(WorkflowExecutionContext context, CancellationToken cancellationToken); + + /// + /// Executes when an activity is about to be executed. + /// + /// The workflow execution context. + /// The activity context containing the activity that is the subject of the event. + /// The cancellation token. + Task OnActivityExecutingAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Called on each activity when an activity has been executed. + /// + /// The workflow execution context. + /// The activity context containing the activity that is the subject of the event. + /// The cancellation token. + Task OnActivityExecutedAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, CancellationToken cancellationToken); + } +} diff --git a/src/Flowsharp.Abstractions/ActivityProviders/TypedActivityProvider.cs b/src/Flowsharp.Abstractions/ActivityProviders/TypedActivityProvider.cs new file mode 100644 index 0000000000..152656c6b6 --- /dev/null +++ b/src/Flowsharp.Abstractions/ActivityProviders/TypedActivityProvider.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Activities; +using Flowsharp.Descriptors; +using Flowsharp.Services; + +namespace Flowsharp.ActivityProviders +{ + /// + /// Provides activities based on implementations that have been registered with the service container. + /// + public class TypedActivityProvider : IActivityProvider + { + private readonly Func> _activitiesFactory; + + public TypedActivityProvider(Func> activitiesFactory) + { + _activitiesFactory = activitiesFactory; + } + + public Task> GetActivityDescriptorsAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_activitiesFactory().Select(ToDescriptor)); + } + + private ActivityDescriptor ToDescriptor(IActivity activity) + { + return new ActivityDescriptor + { + Name = activity.Name, + GetMetadataAsync = activity.ProvideMetadataAsync, + CanExecuteAsync = activity.CanExecuteAsync, + GetOutcomes = activity.GetOutcomes, + ExecuteActivityAsync = activity.ExecuteAsync, + ResumeActivityAsync = activity.ResumeAsync, + ReceiveInputAsync = activity.ReceiveInputAsync, + WorkflowResumedAsync = activity.WorkflowResumedAsync, + WorkflowResumingAsync = activity.WorkflowResumingAsync, + WorkflowStartedAsync = activity.WorkflowStartedAsync, + WorkflowStartingAsync = activity.WorkflowStartingAsync, + OnActivityExecutedAsync = activity.OnActivityExecutedAsync, + OnActivityExecutingAsync = activity.OnActivityExecutingAsync + }; + } + } +} diff --git a/src/Flowsharp.Abstractions/ActivityResults/ActivityExecutionResult.cs b/src/Flowsharp.Abstractions/ActivityResults/ActivityExecutionResult.cs new file mode 100644 index 0000000000..cce7c58fe5 --- /dev/null +++ b/src/Flowsharp.Abstractions/ActivityResults/ActivityExecutionResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Models; + +namespace Flowsharp.ActivityResults +{ + public abstract class ActivityExecutionResult : IActivityExecutionResult + { + public virtual Task ExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + Execute(workflowContext); + return Task.CompletedTask; + } + + protected virtual void Execute(WorkflowExecutionContext workflowContext) + { + throw new NotImplementedException("You must either implement ExecuteAsync or Execute"); + } + } +} diff --git a/src/Flowsharp.Abstractions/ActivityResults/HaltResult.cs b/src/Flowsharp.Abstractions/ActivityResults/HaltResult.cs new file mode 100644 index 0000000000..5a73e78f0b --- /dev/null +++ b/src/Flowsharp.Abstractions/ActivityResults/HaltResult.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Models; + +namespace Flowsharp.ActivityResults +{ + /// + /// Halts workflow execution. + /// + public class HaltResult : ActivityExecutionResult + { + public override async Task ExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var currentActivity = workflowContext.CurrentExecutingActivity; + + if (workflowContext.IsFirstPass) + { + // Resume immediately when this is the first pass. + var result = await currentActivity.ActivityDescriptor.ResumeActivityAsync(workflowContext, currentActivity, cancellationToken); + workflowContext.IsFirstPass = false; + + await result.ExecuteAsync(workflowContext, cancellationToken); + } + else + { + // Block on this activity. + workflowContext.BlockingActivities.Add(currentActivity); + } + } + } +} diff --git a/src/Flowsharp.Abstractions/ActivityResults/IActivityExecutionResult.cs b/src/Flowsharp.Abstractions/ActivityResults/IActivityExecutionResult.cs new file mode 100644 index 0000000000..b4761d5569 --- /dev/null +++ b/src/Flowsharp.Abstractions/ActivityResults/IActivityExecutionResult.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Models; + +namespace Flowsharp.ActivityResults +{ + public interface IActivityExecutionResult + { + Task ExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken); + } +} diff --git a/src/Flowsharp.Abstractions/ActivityResults/NoopResult.cs b/src/Flowsharp.Abstractions/ActivityResults/NoopResult.cs new file mode 100644 index 0000000000..7948c47886 --- /dev/null +++ b/src/Flowsharp.Abstractions/ActivityResults/NoopResult.cs @@ -0,0 +1,15 @@ +using Flowsharp.Models; + +namespace Flowsharp.ActivityResults +{ + /// + /// A result that does nothing. + /// + public class NoopResult : ActivityExecutionResult + { + protected override void Execute(WorkflowExecutionContext workflowContext) + { + // Noop. + } + } +} diff --git a/src/Flowsharp.Abstractions/ActivityResults/OutcomeResult.cs b/src/Flowsharp.Abstractions/ActivityResults/OutcomeResult.cs new file mode 100644 index 0000000000..c1f11050fd --- /dev/null +++ b/src/Flowsharp.Abstractions/ActivityResults/OutcomeResult.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using Flowsharp.Models; + +namespace Flowsharp.ActivityResults +{ + /// + /// An activity execution result that sets the next outcomes to execute. + /// + public class OutcomeResult : ActivityExecutionResult + { + private readonly IEnumerable outcomeNames; + + public OutcomeResult(IEnumerable names) + { + outcomeNames = names.ToList(); + } + + protected override void Execute(WorkflowExecutionContext workflowContext) + { + var workflowType = workflowContext.WorkflowType; + var currentActivity = workflowContext.CurrentExecutingActivity; + + foreach (var outcome in outcomeNames) + { + // Look for next activity in the graph. + var transition = workflowType.Transitions.FirstOrDefault(x => x.From.ActivityId == currentActivity.ActivityType.Id && x.From.OutcomeName == outcome); + + if (transition != null) + { + var destinationActivity = workflowContext.Activities.Values.Single(x => x.ActivityType.Id == transition.To.ActivityId); + workflowContext.PushScheduledActivity(destinationActivity); + } + } + } + } +} diff --git a/src/Flowsharp.Abstractions/Descriptors/ActivityDescriptor.cs b/src/Flowsharp.Abstractions/Descriptors/ActivityDescriptor.cs new file mode 100644 index 0000000000..2d2c62428b --- /dev/null +++ b/src/Flowsharp.Abstractions/Descriptors/ActivityDescriptor.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.ActivityResults; +using Flowsharp.Models; + +namespace Flowsharp.Descriptors +{ + public class ActivityDescriptor + { + public string Name { get; set; } + + /// + /// Provides metadata about the specified activity. + /// + public Func GetMetadataAsync { get; set; } + + /// + /// Returns a list of possible outcomes when the activity is executed. + /// + public Func> GetOutcomes { get; set; } + + /// + /// Returns a value of whether the specified activity can execute. + /// + public Func> CanExecuteAsync { get; set; } + + /// + /// Executes the specified activity. + /// + public Func> ExecuteActivityAsync { get; set; } + + /// + /// Resumes the specified activity. + /// + public Func> ResumeActivityAsync { get; set; } + + /// + /// Executes before a workflow starts or resumes, giving activities an opportunity to read and store any values of interest. + /// + public Func, CancellationToken, Task> ReceiveInputAsync { get; set; } + + /// + /// Executes when a workflow is about to start. + /// + public Func WorkflowStartingAsync { get; set; } + + /// + /// Executes when a workflow has started. + /// + public Func WorkflowStartedAsync { get; set; } + + /// + /// Executes when a workflow is about to be resumed. + /// + public Func WorkflowResumingAsync { get; set; } + + /// + /// Executes when a workflow is resumed. + /// + public Func WorkflowResumedAsync { get; set; } + + /// + /// Executes when an activity is about to be executed. + /// + /// The activity for which the event is invoked. This is not necessarily the activity that is about to be executed. + /// The activity context containing the activity that is the subject of the event. + public Func OnActivityExecutingAsync { get; set; } + + /// + /// Called on each activity when an activity has been executed. + /// + /// The activity for which the event is invoked. This is not necessarily the activity that is about to be executed. + /// The activity context containing the activity that is the subject of the event. + public Func OnActivityExecutedAsync { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Extensions/ExceptionExtensions.cs b/src/Flowsharp.Abstractions/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000000..bd48db0cbf --- /dev/null +++ b/src/Flowsharp.Abstractions/Extensions/ExceptionExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace Flowsharp.Extensions +{ + public static class ExceptionExtensions + { + public static bool IsFatal(this Exception ex) + { + return + ex is OutOfMemoryException || + ex is SecurityException || + ex is SEHException; + } + } +} diff --git a/src/Flowsharp.Abstractions/Extensions/InvokeExtensions.cs b/src/Flowsharp.Abstractions/Extensions/InvokeExtensions.cs new file mode 100644 index 0000000000..38ed6091c0 --- /dev/null +++ b/src/Flowsharp.Abstractions/Extensions/InvokeExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Flowsharp.Extensions +{ + public static class InvokeExtensions + { + /// + /// Safely invoke methods by catching non fatal exceptions and logging them. + /// + public static void Invoke(this IEnumerable events, Action dispatch, ILogger logger) + { + foreach (var sink in events) + { + try + { + dispatch(sink); + } + catch (Exception ex) + { + HandleException(ex, logger, typeof(TEvents).Name, sink.GetType().FullName); + } + } + } + + /// + /// Safely invoke methods by catching non fatal exceptions and logging them. + /// + public static async Task InvokeAsync(this IEnumerable events, Func dispatch, ILogger logger) + { + foreach (var sink in events) + { + try + { + await dispatch(sink); + } + catch (Exception ex) + { + HandleException(ex, logger, typeof(TEvents).Name, sink.GetType().FullName); + } + } + } + + public static void HandleException(Exception ex, ILogger logger, string sourceType, string method) + { + if (ex.IsFatal()) + throw ex; + + logger.LogError(ex, "{Type} thrown from {Method} by {Exception}", + sourceType, + method, + ex.GetType().Name); + } + } +} diff --git a/src/Flowsharp.Abstractions/Flowsharp.Abstractions.csproj b/src/Flowsharp.Abstractions/Flowsharp.Abstractions.csproj new file mode 100644 index 0000000000..d7714b8349 --- /dev/null +++ b/src/Flowsharp.Abstractions/Flowsharp.Abstractions.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + Flowsharp + + + + + + + + + diff --git a/src/Flowsharp.Abstractions/Json/LocalizedStringConverter.cs b/src/Flowsharp.Abstractions/Json/LocalizedStringConverter.cs new file mode 100644 index 0000000000..bdfff3e58b --- /dev/null +++ b/src/Flowsharp.Abstractions/Json/LocalizedStringConverter.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.Localization; +using Newtonsoft.Json; + +namespace Flowsharp.Json +{ + /// + /// Serializes the to a simple string using the translated text. + /// + public class LocalizedStringConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(LocalizedString); + } + + public override bool CanRead => false; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var localizedString = (LocalizedString)value; + writer.WriteValue(localizedString.Value); + } + } +} diff --git a/src/Flowsharp.Abstractions/Models/ActivityExecutionContext.cs b/src/Flowsharp.Abstractions/Models/ActivityExecutionContext.cs new file mode 100644 index 0000000000..9f1461f462 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/ActivityExecutionContext.cs @@ -0,0 +1,19 @@ +using Flowsharp.Descriptors; +using Newtonsoft.Json.Linq; + +namespace Flowsharp.Models +{ + public class ActivityExecutionContext + { + public ActivityExecutionContext(ActivityType activityType, ActivityDescriptor activityDescriptor) + { + ActivityType = activityType; + ActivityDescriptor = activityDescriptor; + State = new JObject(activityType.State); + } + + public ActivityType ActivityType { get; } + public ActivityDescriptor ActivityDescriptor { get; } + public JObject State { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/ActivityMetadata.cs b/src/Flowsharp.Abstractions/Models/ActivityMetadata.cs new file mode 100644 index 0000000000..a4f9542ab4 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/ActivityMetadata.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Localization; + +namespace Flowsharp.Abstractions.Models +{ + public class ActivityMetadata + { + public LocalizedString DisplayName { get; set; } + public LocalizedString Category { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/ActivityMetadataContext.cs b/src/Flowsharp.Abstractions/Models/ActivityMetadataContext.cs new file mode 100644 index 0000000000..ae4c60bf34 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/ActivityMetadataContext.cs @@ -0,0 +1,9 @@ +using Flowsharp.Abstractions.Models; + +namespace Flowsharp.Models +{ + public class ActivityMetadataContext + { + public ActivityMetadata Metadata { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/ActivityType.cs b/src/Flowsharp.Abstractions/Models/ActivityType.cs new file mode 100644 index 0000000000..ad359611e7 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/ActivityType.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json.Linq; + +namespace Flowsharp.Models +{ + public class ActivityType + { + public ActivityType(string id, string name, JObject state = null) + { + Id = id; + Name = name; + State = state ?? new JObject(); + } + + public string Id { get; } + public string Name { get; } + public JObject State { get; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/DestinationEndpoint.cs b/src/Flowsharp.Abstractions/Models/DestinationEndpoint.cs new file mode 100644 index 0000000000..be332d605d --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/DestinationEndpoint.cs @@ -0,0 +1,6 @@ +namespace Flowsharp.Models +{ + public class DestinationEndpoint : Endpoint + { + } +} diff --git a/src/Flowsharp.Abstractions/Models/Endpoint.cs b/src/Flowsharp.Abstractions/Models/Endpoint.cs new file mode 100644 index 0000000000..96daadac68 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/Endpoint.cs @@ -0,0 +1,7 @@ +namespace Flowsharp.Models +{ + public class Endpoint + { + public string ActivityId { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/Outcome.cs b/src/Flowsharp.Abstractions/Models/Outcome.cs new file mode 100644 index 0000000000..c3b7c9c786 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/Outcome.cs @@ -0,0 +1,24 @@ +using Flowsharp.Json; +using Microsoft.Extensions.Localization; +using Newtonsoft.Json; + +namespace Flowsharp.Models +{ + public class Outcome + { + public Outcome(LocalizedString displayName) : this(displayName.Name, displayName) + { + } + + public Outcome(string name, LocalizedString displayName) + { + Name = name; + DisplayName = displayName; + } + + public string Name { get; } + + [JsonConverter(typeof(LocalizedStringConverter))] + public LocalizedString DisplayName { get; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/SourceEndpoint.cs b/src/Flowsharp.Abstractions/Models/SourceEndpoint.cs new file mode 100644 index 0000000000..164d055241 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/SourceEndpoint.cs @@ -0,0 +1,7 @@ +namespace Flowsharp.Models +{ + public class SourceEndpoint : Endpoint + { + public string OutcomeName { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/Transition.cs b/src/Flowsharp.Abstractions/Models/Transition.cs new file mode 100644 index 0000000000..e99d8abe66 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/Transition.cs @@ -0,0 +1,8 @@ +namespace Flowsharp.Models +{ + public class Transition + { + public SourceEndpoint From { get; set; } + public DestinationEndpoint To { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Models/WorkflowExecutionContext.cs b/src/Flowsharp.Abstractions/Models/WorkflowExecutionContext.cs new file mode 100644 index 0000000000..e610255efb --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/WorkflowExecutionContext.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Flowsharp.Descriptors; + +namespace Flowsharp.Models +{ + public class WorkflowExecutionContext + { + public WorkflowExecutionContext(IDictionary activityDescriptorDictionary, WorkflowType workflowType, WorkflowStatus status) + { + WorkflowType = workflowType; + Activities = workflowType.Activities.ToDictionary(x => x.Id, x => new ActivityExecutionContext(x, activityDescriptorDictionary[x.Name])); + BlockingActivities = new List(); + Status = status; + IsFirstPass = true; + + scheduledActivities = new Stack(); + } + + private readonly Stack scheduledActivities; + + public WorkflowType WorkflowType { get; } + public IDictionary Activities { get; } + public ICollection BlockingActivities { get; } + public WorkflowStatus Status { get; set; } + public bool HasScheduledActivities => scheduledActivities.Any(); + public bool IsFirstPass { get; set; } + public ActivityExecutionContext CurrentExecutingActivity { get; private set; } + + public void PushScheduledActivity(ActivityExecutionContext activityExecutionContext) + { + scheduledActivities.Push(activityExecutionContext); + } + + public ActivityExecutionContext PopScheduledActivity() + { + return CurrentExecutingActivity = scheduledActivities.Pop(); + } + + public void Fault(Exception exception, ActivityExecutionContext activity) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Flowsharp.Abstractions/Models/WorkflowStatus.cs b/src/Flowsharp.Abstractions/Models/WorkflowStatus.cs new file mode 100644 index 0000000000..658dd047f2 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/WorkflowStatus.cs @@ -0,0 +1,14 @@ +namespace Flowsharp.Models +{ + public enum WorkflowStatus + { + Idle, + Starting, + Resuming, + Executing, + Halted, + Finished, + Faulted, + Aborted + } +} diff --git a/src/Flowsharp.Abstractions/Models/WorkflowType.cs b/src/Flowsharp.Abstractions/Models/WorkflowType.cs new file mode 100644 index 0000000000..b268e16952 --- /dev/null +++ b/src/Flowsharp.Abstractions/Models/WorkflowType.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Flowsharp.Models +{ + public class WorkflowType + { + public string Name { get; set; } + public ICollection Activities { get; set; } + public ICollection Transitions { get; set; } + } +} diff --git a/src/Flowsharp.Abstractions/Services/IActivityLibrary.cs b/src/Flowsharp.Abstractions/Services/IActivityLibrary.cs new file mode 100644 index 0000000000..f6d85dea77 --- /dev/null +++ b/src/Flowsharp.Abstractions/Services/IActivityLibrary.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Descriptors; + +namespace Flowsharp.Services +{ + public interface IActivityLibrary + { + Task> GetActivityDescriptorsAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Flowsharp.Abstractions/Services/IActivityProvider.cs b/src/Flowsharp.Abstractions/Services/IActivityProvider.cs new file mode 100644 index 0000000000..4c2e598314 --- /dev/null +++ b/src/Flowsharp.Abstractions/Services/IActivityProvider.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Descriptors; + +namespace Flowsharp.Services +{ + /// + /// Implementors provide available activity descriptors. + /// + public interface IActivityProvider + { + Task> GetActivityDescriptorsAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Flowsharp.Abstractions/Services/IWorkflowInvoker.cs b/src/Flowsharp.Abstractions/Services/IWorkflowInvoker.cs new file mode 100644 index 0000000000..444cdaf465 --- /dev/null +++ b/src/Flowsharp.Abstractions/Services/IWorkflowInvoker.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Models; + +namespace Flowsharp.Services +{ + public interface IWorkflowInvoker + { + Task InvokeAsync(WorkflowExecutionContext workflowContext, string startActivityId, CancellationToken cancellationToken); + } +} diff --git a/src/Flowsharp.Core/Activities/WriteLine.cs b/src/Flowsharp.Core/Activities/WriteLine.cs new file mode 100644 index 0000000000..081d5b6471 --- /dev/null +++ b/src/Flowsharp.Core/Activities/WriteLine.cs @@ -0,0 +1,19 @@ +using System; +using Flowsharp.Activities; +using Flowsharp.ActivityResults; +using Flowsharp.Models; + +namespace Flowsharp.ActivityProviders +{ + /// + /// Provides activities based on implementations that have been registered with the service container. + /// + public class WriteLine : Activity + { + protected override ActivityExecutionResult Execute(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext) + { + Console.WriteLine("Hello World!"); + return Outcomes("Done"); + } + } +} diff --git a/src/Flowsharp.Core/Flowsharp.Core.csproj b/src/Flowsharp.Core/Flowsharp.Core.csproj new file mode 100644 index 0000000000..c7919e2060 --- /dev/null +++ b/src/Flowsharp.Core/Flowsharp.Core.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + Flowsharp + + + + + + + + + + + + diff --git a/src/Flowsharp.Core/Services/ActivityLibrary.cs b/src/Flowsharp.Core/Services/ActivityLibrary.cs new file mode 100644 index 0000000000..227b379521 --- /dev/null +++ b/src/Flowsharp.Core/Services/ActivityLibrary.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Descriptors; + +namespace Flowsharp.Services +{ + public class ActivityLibrary : IActivityLibrary + { + private readonly IEnumerable providers; + + public ActivityLibrary(IEnumerable providers) + { + this.providers = providers; + } + + public async Task> GetActivityDescriptorsAsync(CancellationToken cancellationToken) + { + var tasks = providers.Select(x => x.GetActivityDescriptorsAsync(cancellationToken)).ToList(); + var results = await Task.WhenAll(tasks); + + return results.SelectMany(x => x); + } + } + + public static class ActivityLibraryExtensions + { + public static async Task> GetActivityDescriptorDictionaryAsync(this IActivityLibrary activityLibrary, CancellationToken cancellationToken) + { + var activityDescriptors = await activityLibrary.GetActivityDescriptorsAsync(cancellationToken); + return activityDescriptors.ToDictionary(x => x.Name); + } + } +} diff --git a/src/Flowsharp.Core/Services/WorkflowInvoker.cs b/src/Flowsharp.Core/Services/WorkflowInvoker.cs new file mode 100644 index 0000000000..6447c1bb14 --- /dev/null +++ b/src/Flowsharp.Core/Services/WorkflowInvoker.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.ActivityResults; +using Flowsharp.Extensions; +using Flowsharp.Models; +using Microsoft.Extensions.Logging; + +namespace Flowsharp.Services +{ + public class WorkflowInvoker : IWorkflowInvoker + { + public WorkflowInvoker(ILogger logger) + { + this.logger = logger; + } + + private readonly ILogger logger; + + public async Task InvokeAsync(WorkflowExecutionContext workflowContext, string startActivityId, CancellationToken cancellationToken) + { + var isResuming = workflowContext.Status == WorkflowStatus.Resuming; + var startActivity = workflowContext.Activities[startActivityId]; + + workflowContext.Status = WorkflowStatus.Executing; + workflowContext.PushScheduledActivity(startActivity); + + while (workflowContext.HasScheduledActivities) + { + var currentActivity = workflowContext.PopScheduledActivity(); + + if (!await ExecuteActivityAsync(workflowContext, currentActivity, isResuming, cancellationToken)) + break; + + workflowContext.IsFirstPass = false; + isResuming = false; + } + + workflowContext.Status = workflowContext.BlockingActivities.Any() ? WorkflowStatus.Halted : WorkflowStatus.Finished; + } + + private async Task ExecuteActivityAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, bool isResuming, CancellationToken cancellationToken) + { + try + { + await InvokeActivitiesAsync(workflowContext, x => x.ActivityDescriptor.OnActivityExecutingAsync(workflowContext, activityContext, cancellationToken)); + + if (cancellationToken.IsCancellationRequested) + { + workflowContext.Status = WorkflowStatus.Aborted; + return false; + } + + var result = await ExecuteOrResumeActivityAsync(workflowContext, activityContext, isResuming, cancellationToken); + await InvokeActivitiesAsync(workflowContext, x => x.ActivityDescriptor.OnActivityExecutedAsync(workflowContext, activityContext, cancellationToken)); + await result.ExecuteAsync(workflowContext, cancellationToken); + + } + catch (Exception ex) + { + FaultWorkflow(workflowContext, activityContext, ex); + } + + return true; + } + + private void FaultWorkflow(WorkflowExecutionContext workflowContext, ActivityExecutionContext activityContext, Exception ex) + { + logger.LogError( + ex, + "An unhandled error occurred while executing an activity. Workflow ID: '{WorkflowTypeId}'. Activity: '{ActivityId}', '{ActivityName}'. Putting the workflow in the faulted state.", + workflowContext.WorkflowType.Name, + activityContext.ActivityType.Id, + activityContext.ActivityType.Name + ); + workflowContext.Fault(ex, activityContext); + } + + private async Task ExecuteOrResumeActivityAsync(WorkflowExecutionContext workflowContext, ActivityExecutionContext activity, bool isResuming, CancellationToken cancellationToken) + { + if (!isResuming) + { + // Execute the current activity. + return await activity.ActivityDescriptor.ExecuteActivityAsync(workflowContext, activity, cancellationToken); + } + else + { + // Resume the current activity. + return await activity.ActivityDescriptor.ResumeActivityAsync(workflowContext, activity, cancellationToken); + } + } + + private async Task InvokeActivitiesAsync(WorkflowExecutionContext workflowContext, Func action) + { + await workflowContext.Activities.Values.InvokeAsync(action, logger); + } + } +} diff --git a/src/Flowsharp.Fluid/Flowsharp.Fluid.csproj b/src/Flowsharp.Fluid/Flowsharp.Fluid.csproj new file mode 100644 index 0000000000..633d246d25 --- /dev/null +++ b/src/Flowsharp.Fluid/Flowsharp.Fluid.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Flowsharp.Fluid/WorkflowBuilder.cs b/src/Flowsharp.Fluid/WorkflowBuilder.cs new file mode 100644 index 0000000000..a4e310cb28 --- /dev/null +++ b/src/Flowsharp.Fluid/WorkflowBuilder.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Flowsharp.Models; + +namespace Flowsharp.Fluid +{ + public class WorkflowBuilder + { + public WorkflowBuilder() + { + activityTypes = new List(); + } + + private IList activityTypes; + + } +} \ No newline at end of file diff --git a/src/Flowsharp.Samples.Console/Flowsharp.Samples.Console.csproj b/src/Flowsharp.Samples.Console/Flowsharp.Samples.Console.csproj new file mode 100644 index 0000000000..0e7b2a8e71 --- /dev/null +++ b/src/Flowsharp.Samples.Console/Flowsharp.Samples.Console.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp2.1 + 7.1 + + + + + + + + diff --git a/src/Flowsharp.Samples.Console/Program.cs b/src/Flowsharp.Samples.Console/Program.cs new file mode 100644 index 0000000000..c7afb65c48 --- /dev/null +++ b/src/Flowsharp.Samples.Console/Program.cs @@ -0,0 +1,34 @@ +using System.Threading; +using System.Threading.Tasks; +using Flowsharp.Activities; +using Flowsharp.ActivityProviders; +using Flowsharp.Models; +using Flowsharp.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Flowsharp.Samples.Console +{ + class Program + { + static async Task Main() + { + var workflowType = new WorkflowType + { + Activities = new[] + { + new ActivityType("1", "WriteLine") + }, + Transitions = new Transition[0] + }; + + var typedActivityProvider = new TypedActivityProvider(() => new IActivity[] { new WriteLine() } ); + var activityLibrary = new ActivityLibrary(new[]{ typedActivityProvider }); + var dictionary = await activityLibrary.GetActivityDescriptorDictionaryAsync(CancellationToken.None); + var workflowContext = new WorkflowExecutionContext(dictionary, workflowType, WorkflowStatus.Idle); + var invoker = new WorkflowInvoker(new Logger(new NullLoggerFactory())); + + await invoker.InvokeAsync(workflowContext, "1", CancellationToken.None); + } + } +}