From 67be790c7de99aa59bce9e418c3fba8cc3307304 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 3 Jan 2021 11:06:11 +0100 Subject: [PATCH] Fix access to container properties and improve object type conversion --- .../Extensions/ObjectConverter.cs | 8 +++-- .../Models/ActivityExecutionContext.cs | 8 +++++ .../Finish/FinishBuilderExtensions.cs | 1 + .../Activities/ControlFlow/For/For.cs | 2 +- .../Builders/CompositeActivityBuilder.cs | 9 ++--- .../Elsa.Core/Services/ActivityFactory.cs | 2 +- .../Elsa.Core/Services/ActivityTypeService.cs | 13 +++++-- .../Activities/CountdownActivity.cs | 36 +++++++++++++++++++ ...untDownActivity.cs => NavigateActivity.cs} | 2 +- .../Program.cs | 5 ++- .../Workflows/CompositionWorkflow.cs | 9 ++--- 11 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs rename src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/{CountDownActivity.cs => NavigateActivity.cs} (94%) diff --git a/src/core/Elsa.Abstractions/Extensions/ObjectConverter.cs b/src/core/Elsa.Abstractions/Extensions/ObjectConverter.cs index 235238475..a95985c8b 100644 --- a/src/core/Elsa.Abstractions/Extensions/ObjectConverter.cs +++ b/src/core/Elsa.Abstractions/Extensions/ObjectConverter.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using NodaTime; using NodaTime.Text; @@ -21,7 +22,10 @@ namespace Elsa return (T?)((object?)DurationPattern.JsonRoundtrip.Parse(value!.ToString()).Value)!; var converter = TypeDescriptor.GetConverter(typeof(T)); - return converter.CanConvertFrom(value.GetType()) ? (T?) converter.ConvertFrom(value) : default; + if (converter.CanConvertFrom(value.GetType())) + return (T?)converter.ConvertFrom(value); + + return (T?)Convert.ChangeType(value, typeof(T)); } } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs index 00ba517d2..0f8d1fda2 100644 --- a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs @@ -31,10 +31,18 @@ namespace Elsa.Services.Models public IServiceScope ServiceScope { get; } public IActivityBlueprint ActivityBlueprint { get; } public ActivityInstance ActivityInstance { get; } + public ActivityInstance? ParentActivityInstance => WorkflowExecutionContext.WorkflowInstance.Activities.FirstOrDefault(x => x.Id == ActivityBlueprint.Parent?.Id); public IReadOnlyCollection Outcomes { get; set; } public object? Input { get; } public CancellationToken CancellationToken { get; } public JObject Data => ActivityInstance.Data; + public JObject? ParentData => ParentActivityInstance?.Data; + + public T? GetParentState(string key) + { + var parentData = ParentData; + return parentData == null ? default : parentData.GetState(key); + } public object? Output { diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Finish/FinishBuilderExtensions.cs b/src/core/Elsa.Core/Activities/ControlFlow/Finish/FinishBuilderExtensions.cs index 2dcb0b17c..13fa4919e 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/Finish/FinishBuilderExtensions.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Finish/FinishBuilderExtensions.cs @@ -12,6 +12,7 @@ namespace Elsa.Activities.ControlFlow public static IActivityBuilder Finish(this IBuilder builder, Func> output) => builder.Finish(activity => activity.WithOutput(output)); public static IActivityBuilder Finish(this IBuilder builder, Func output) => builder.Finish(activity => activity.WithOutput(output)); public static IActivityBuilder Finish(this IBuilder builder, Func output) => builder.Finish(activity => activity.WithOutput(output)); + public static IActivityBuilder Finish(this IBuilder builder, object? output) => builder.Finish(activity => activity.WithOutput(output)); public static IActivityBuilder Finish(this IBuilder builder, Func> outcome) => builder.Finish(activity => activity.WithOutcome(outcome)); public static IActivityBuilder Finish(this IBuilder builder, Func outcome) => builder.Finish(activity => activity.WithOutcome(outcome)); diff --git a/src/core/Elsa.Core/Activities/ControlFlow/For/For.cs b/src/core/Elsa.Core/Activities/ControlFlow/For/For.cs index 1830bdb43..c6169bf82 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/For/For.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/For/For.cs @@ -48,7 +48,7 @@ namespace Elsa.Activities.ControlFlow if (loop) { CurrentValue = currentValue + Step; - return Combine(Schedule(Id), Output(currentValue), Outcome(OutcomeNames.Iterate)); + return Outcome(OutcomeNames.Iterate, currentValue); } CurrentValue = null; diff --git a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs index 1dfe9af26..5ad602fe6 100644 --- a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs +++ b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs @@ -141,6 +141,11 @@ namespace Elsa.Builders var activityBlueprintDictionary = activityBlueprints.ToDictionary(x => x.Id); connections.AddRange(ConnectionBuilders.Select(x => new Connection(activityBlueprintDictionary[x.Source().ActivityId], activityBlueprintDictionary[x.Target().ActivityId], x.Outcome))); + + compositeActivityBlueprint.Connections = connections; + compositeActivityBlueprint.Activities = activityBlueprints; + compositeActivityBlueprint.ActivityPropertyProviders = new ActivityPropertyProviders(activityPropertyProviders); + // Build composite activities. var compositeActivityBuilders = activityBuilders.Where(x => typeof(CompositeActivity).IsAssignableFrom(x.ActivityType)); BuildCompositeActivities(compositeActivityBuilders, activityBlueprints, connections, activityPropertyProviders); @@ -150,10 +155,6 @@ namespace Elsa.Builders .Select(x => (x.ActivityId, x.PropertyValueProviders)) .ToDictionary(x => x.ActivityId!, x => x.PropertyValueProviders!)); - compositeActivityBlueprint.Connections = connections; - compositeActivityBlueprint.Activities = activityBlueprints; - compositeActivityBlueprint.ActivityPropertyProviders = new ActivityPropertyProviders(activityPropertyProviders); - return compositeActivityBlueprint; } diff --git a/src/core/Elsa.Core/Services/ActivityFactory.cs b/src/core/Elsa.Core/Services/ActivityFactory.cs index e494f81a4..0dfbb3e41 100644 --- a/src/core/Elsa.Core/Services/ActivityFactory.cs +++ b/src/core/Elsa.Core/Services/ActivityFactory.cs @@ -6,7 +6,7 @@ namespace Elsa.Services { public class ActivityFactory : IActivityFactory { - public ActivityInstance Instantiate(IActivityBlueprint activityBlueprint) => new ActivityInstance( + public ActivityInstance Instantiate(IActivityBlueprint activityBlueprint) => new( activityBlueprint.Id, activityBlueprint.Type, null, diff --git a/src/core/Elsa.Core/Services/ActivityTypeService.cs b/src/core/Elsa.Core/Services/ActivityTypeService.cs index 5013ef976..df32eca13 100644 --- a/src/core/Elsa.Core/Services/ActivityTypeService.cs +++ b/src/core/Elsa.Core/Services/ActivityTypeService.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Elsa.ActivityProviders; +using Elsa.Exceptions; using Elsa.Services.Models; namespace Elsa.Services @@ -19,8 +20,16 @@ namespace Elsa.Services } public async ValueTask> GetActivityTypesAsync(CancellationToken cancellationToken) => (await GetDictionaryAsync(cancellationToken)).Values; - public async ValueTask GetActivityTypeAsync(string type, CancellationToken cancellationToken) => (await GetDictionaryAsync(cancellationToken))[type]; - + public async ValueTask GetActivityTypeAsync(string type, CancellationToken cancellationToken) + { + var dictionary = await GetDictionaryAsync(cancellationToken); + + if (!dictionary.ContainsKey(type)) + throw new WorkflowException($"The activity type '{type}' has not been registered. Did you forget to register it with ElsaOptions?"); + + return dictionary[type]; + } + public async ValueTask ActivateActivityAsync(IActivityBlueprint activityBlueprint, CancellationToken cancellationToken = default) { var type = await GetActivityTypeAsync(activityBlueprint.Type, cancellationToken); diff --git a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs new file mode 100644 index 000000000..fcde86bea --- /dev/null +++ b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountdownActivity.cs @@ -0,0 +1,36 @@ +using Elsa.Activities.Console; +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Timers; +using Elsa.Attributes; +using Elsa.Builders; +using Elsa.Services; +using Elsa.Services.Models; +using NodaTime; + +namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Activities +{ + /// + /// Custom activities that inherit from declare their own mini-workflow. + /// + public class CountdownActivity : CompositeActivity + { + // Exposed properties on composite activities must store their values in the State property bag so that it gets persisted & available to child activities. + // Activities are scheduled one by one, which means that when the child activities are executed, the container activity (such as this one) will not be in memory - the default value will be captured when the property is used directly. + [ActivityProperty(Hint = "The start number.")] + public int Start + { + get => GetState(); + set => SetState(value); + } + + public override void Build(ICompositeActivityBuilder activity) + { + activity + .For(context => context.GetParentState(nameof(Start)), _ => 0, _ => -1, iterate => + { + iterate.WriteLine(context => $"{context.GetInput()}..."); + }, Operator.GreaterThan) + .WriteLine("Happy New Year!"); + } + } +} \ No newline at end of file diff --git a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountDownActivity.cs b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/NavigateActivity.cs similarity index 94% rename from src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountDownActivity.cs rename to src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/NavigateActivity.cs index 21dff6b79..4ed46de5a 100644 --- a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/CountDownActivity.cs +++ b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Activities/NavigateActivity.cs @@ -12,7 +12,7 @@ namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Activities /// Custom activities that inherit from declare their own mini-workflow. /// [Action(Outcomes = new[] { "Left", "Right" })] - public class CountDownActivity : CompositeActivity + public class NavigateActivity : CompositeActivity { public override void Build(ICompositeActivityBuilder activity) { diff --git a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Program.cs b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Program.cs index ee3e5adeb..3917bf1a4 100644 --- a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Program.cs +++ b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Program.cs @@ -3,6 +3,7 @@ using Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Activities; using Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Workflows; using Elsa.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole { @@ -12,9 +13,11 @@ namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole { // Create a service container with Elsa services. var services = new ServiceCollection() + .AddLogging(logging => logging.AddConsole()) .AddElsa(options => options .AddConsoleActivities() - .AddActivity() + .AddActivity() + .AddActivity() .AddWorkflow()) .BuildServiceProvider(); diff --git a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Workflows/CompositionWorkflow.cs b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Workflows/CompositionWorkflow.cs index 18827394a..1cd290155 100644 --- a/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Workflows/CompositionWorkflow.cs +++ b/src/samples/console/Elsa.Samples.ProgrammaticCompositeActivitiesConsole/Workflows/CompositionWorkflow.cs @@ -11,14 +11,15 @@ namespace Elsa.Samples.ProgrammaticCompositeActivitiesConsole.Workflows { public void Build(IWorkflowBuilder workflow) => workflow .WriteLine("Welcome to the Composite Activities demo workflow!") - + // A custom, composite activity - .Then(countDown => + .WriteLine("=Navigation demo=") + .Then(countDown => { countDown.When("Left").WriteLine("Where going left!"); countDown.When("Right").WriteLine("Where going right!"); }) - .WriteLine("Done") - ; + .WriteLine("=Countdown demo=") + .Then(activity => activity.Set(x => x.Start, 10)); } } \ No newline at end of file