diff --git a/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs b/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs index 95c4cfa1d..280115edb 100644 --- a/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs @@ -9,21 +9,6 @@ namespace Elsa { public static class CompositeActivityBlueprintExtensions { - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint) - { - var targetActivityIds = workflowBlueprint.Connections.Select(x => x.Target.Activity?.Id).Distinct().ToLookup(x => x); - - var query = - from activity in workflowBlueprint.Activities - where !targetActivityIds.Contains(activity.Id) - select activity; - - return query; - } - - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint, string activityType) => workflowBlueprint.GetStartActivities().Where(x => x.Type == activityType); - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint, Type activityType) => workflowBlueprint.GetStartActivities(activityType.Name); - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint) where T : IActivity => workflowBlueprint.GetStartActivities(typeof(T)); public static IEnumerable GetEndActivities(this ICompositeActivityBlueprint workflowBlueprint) => workflowBlueprint.Activities.Where(x => !workflowBlueprint.GetOutboundConnections(x.Id).Any()); public static IActivityBlueprint? GetActivity(this ICompositeActivityBlueprint workflowBlueprint, string id) => workflowBlueprint.Activities.FirstOrDefault(x => x.Id == id); public static IEnumerable GetActivities(this ICompositeActivityBlueprint workflowBlueprint, IEnumerable ids) => workflowBlueprint.Activities.Where(x => ids.Contains(x.Id)); diff --git a/src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs b/src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs new file mode 100644 index 000000000..90a7ecad4 --- /dev/null +++ b/src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Extensions +{ + public static class StartActivitiesForCompositeActivityBlueprintProviderExtensions + { + public static IEnumerable GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider, + ICompositeActivityBlueprint workflowBlueprint, + string activityType) + => startActivitiesProvider.GetStartActivities(workflowBlueprint).Where(x => x.Type == activityType); + + public static IEnumerable GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider, + ICompositeActivityBlueprint workflowBlueprint, + Type activityType) + => startActivitiesProvider.GetStartActivities(workflowBlueprint, activityType.Name); + + public static IEnumerable GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider, + ICompositeActivityBlueprint workflowBlueprint) where T : IActivity + => startActivitiesProvider.GetStartActivities(workflowBlueprint, typeof(T)); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs b/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs index 7867b18e2..f43b6cad8 100644 --- a/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs @@ -10,6 +10,8 @@ namespace Elsa GetActivityById(this WorkflowDefinition workflowDefinition, string activityId) => workflowDefinition.Activities.First(x => x.ActivityId == activityId); + // TODO: Consider adding this as an overload for IGetsStartActivitiesForCompositeActivityBlueprint + // The intent is the same and the logic is near-identical. public static IEnumerable GetStartActivities(this WorkflowDefinition workflowDefinition) { var targetActivities = workflowDefinition.Connections diff --git a/src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs b/src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs new file mode 100644 index 000000000..d7afab8bd --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// An object which can create an activity execution context for a specified activity blueprint. + /// + public interface ICreatesActivityExecutionContextForActivityBlueprint + { + /// + /// Creates a activity execution context for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A cancellation token + /// An activity execution context + ActivityExecutionContext CreateActivityExecutionContext(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs b/src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs new file mode 100644 index 000000000..c819a15c1 --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// An object which can create a workflow execution context for a specified workflow blueprint. + /// + public interface ICreatesWorkflowExecutionContextForWorkflowBlueprint + { + /// + /// Creates a workflow execution context for the specified workflow blueprint. + /// + /// A workflow blueprint + /// An optional cancellation token + /// A task for a workflow execution context + Task CreateWorkflowExecutionContextAsync(IWorkflowBlueprint workflowBlueprint, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs b/src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs new file mode 100644 index 000000000..0c6f91c64 --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// An object which gets the starting activities for a specified . + /// + public interface IGetsStartActivitiesForCompositeActivityBlueprint + { + /// + /// Gets a collection of the starting activities for the specified composite activity blueprint. + /// + /// A composite activity blueprint + /// A collection of the blueprint's starting activities + IEnumerable GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs new file mode 100644 index 000000000..8a8e9bf25 --- /dev/null +++ b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// An object which can get a collection of the for a specified activity blueprint and workflow. + /// + public interface IGetsTriggersForActivityBlueprintAndWorkflow + { + /// + /// Gets a collection of the workflow triggers for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A dictionary of all of the activity types (by name) + /// An optional cancellation token + /// A task exposing a collection of workflow triggers for the activity and workflow. + Task> GetTriggersForActivityBlueprintAsync(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + IDictionary activityTypes, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs new file mode 100644 index 000000000..c5f568697 --- /dev/null +++ b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// An object which can get all of the workflow triggers for a collection of workflow blueprints. + /// + public interface IGetsTriggersForWorkflowBlueprints + { + /// + /// Gets the triggers for all of the specified workflow blueprints. + /// + /// The workflow blueprints for which to get triggers. + /// An optional cancellation token. + /// A task which exposes an enumerable collection of workflow triggers. + Task> GetTriggersAsync(IEnumerable workflowBlueprints, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs index 0bd2bbf6a..6b76b0444 100644 --- a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs +++ b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs @@ -14,9 +14,12 @@ namespace Elsa.Builders public class CompositeActivityBuilder : ActivityBuilder, ICompositeActivityBuilder { private readonly Func _compositeActivityBuilderFactory; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public CompositeActivityBuilder(IServiceProvider serviceProvider) + public CompositeActivityBuilder(IServiceProvider serviceProvider, IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); + ServiceProvider = serviceProvider; ActivityBuilders = new List(); ConnectionBuilders = new List(); @@ -58,7 +61,7 @@ namespace Elsa.Builders var valueProviders = propertyValuesBuilder.ValueProviders.ToDictionary( x => x.Key, - x => (IActivityPropertyValueProvider) new DelegateActivityPropertyValueProvider(x.Value)); + x => (IActivityPropertyValueProvider)new DelegateActivityPropertyValueProvider(x.Value)); return New(valueProviders, lineNumber, sourceFile); } @@ -181,7 +184,7 @@ namespace Elsa.Builders using var scope = ServiceProvider.CreateScope(); foreach (var activityBuilder in compositeActivityBuilders) { - var compositeActivity = (CompositeActivity) ActivatorUtilities.CreateInstance(scope.ServiceProvider, activityBuilder.ActivityType); + var compositeActivity = (CompositeActivity)ActivatorUtilities.CreateInstance(scope.ServiceProvider, activityBuilder.ActivityType); var compositeActivityBuilder = _compositeActivityBuilderFactory(); compositeActivityBuilder.ActivityId = activityBuilder.ActivityId; compositeActivity.Build(compositeActivityBuilder); @@ -198,7 +201,7 @@ namespace Elsa.Builders compositeActivityBlueprint.ActivityPropertyProviders = compositeActivityBlueprint.ActivityPropertyProviders; // Connect the composite activity to its starting activities. - var startActivities = compositeActivityBlueprint.GetStartActivities().ToList(); + var startActivities = startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); connections.AddRange(startActivities.Select(x => new Connection(compositeActivityBlueprint, x, CompositeActivity.Enter))); } } diff --git a/src/core/Elsa.Core/Builders/WorkflowBuilder.cs b/src/core/Elsa.Core/Builders/WorkflowBuilder.cs index 1aadcf86d..ad425f07b 100644 --- a/src/core/Elsa.Core/Builders/WorkflowBuilder.cs +++ b/src/core/Elsa.Core/Builders/WorkflowBuilder.cs @@ -10,7 +10,7 @@ namespace Elsa.Builders { public class WorkflowBuilder : CompositeActivityBuilder, IWorkflowBuilder { - public WorkflowBuilder(IIdGenerator idGenerator, IServiceProvider serviceProvider) : base(serviceProvider) + public WorkflowBuilder(IIdGenerator idGenerator, IServiceProvider serviceProvider, IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) : base(serviceProvider, startingActivitiesProvider) { Version = 1; Variables = new Variables(); diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index bbb55012c..8be3dfde3 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -109,6 +109,9 @@ namespace Microsoft.Extensions.DependencyInjection .AddTransient() .AddActivityTypeProvider() .AddScoped() + .AddTransient() + .AddTransient() + .AddTransient() ; // Serialization. @@ -143,6 +146,8 @@ namespace Microsoft.Extensions.DependencyInjection .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddTransient() .AddSingleton() .AddScoped() .AddBookmarkProvider() diff --git a/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs new file mode 100644 index 000000000..02c80c835 --- /dev/null +++ b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// Default implementation of . + /// + public class ActivityExecutionContextForActivityBlueprintFactory : ICreatesActivityExecutionContextForActivityBlueprint + { + readonly IServiceProvider serviceProvider; + + public ActivityExecutionContextForActivityBlueprintFactory(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// + /// Creates a activity execution context for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A cancellation token + /// An activity execution context + public ActivityExecutionContext CreateActivityExecutionContext(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) + { + return new ActivityExecutionContext(serviceProvider, workflowExecutionContext, activityBlueprint, null, false, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs new file mode 100644 index 000000000..ab59f9abb --- /dev/null +++ b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// Default implementation of . + /// + public class StartActivitiesForCompositeActivityBlueprintProvider : IGetsStartActivitiesForCompositeActivityBlueprint + { + /// + /// Gets a collection of the starting activities for the specified composite activity blueprint. + /// + /// A composite activity blueprint + /// A collection of the blueprint's starting activities + public IEnumerable GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint) + { + var activityIdsThatAreNotStartingActivities = GetAllActivityIdsWhichHaveInboundConnections(compositeActivityBlueprint); + + var query = from activity in compositeActivityBlueprint.Activities + where !activityIdsThatAreNotStartingActivities.Contains(activity.Id) + select activity; + + return query; + } + + /// + /// This method gets activities that have inbound connections. + /// + /// + /// + /// "Start activities" are those with no inbound connections; IE no workflow connection-target will point to a start activity. + /// What this method returns is essentially a blacklist of activity IDs which are not starting activities. + /// + /// + /// A composite activity blueprint + /// A lookup of activity IDs which are not starting activities + ILookup GetAllActivityIdsWhichHaveInboundConnections(ICompositeActivityBlueprint compositeActivityBlueprint) + { + return compositeActivityBlueprint.Connections + .Select(x => x.Target.Activity?.Id) + .Distinct() + .ToLookup(x => x); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs b/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs index 6de34dfaa..609c56f83 100644 --- a/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs +++ b/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs @@ -14,21 +14,25 @@ namespace Elsa.Services { private readonly IActivityTypeService _activityTypeService; private readonly ILogger _logger; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService, ILogger logger) + public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService, + ILogger logger, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new System.ArgumentNullException(nameof(startingActivitiesProvider)); _activityTypeService = activityTypeService; _logger = logger; } - + public async Task CreateWorkflowBlueprintAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken) { var manyActivityBlueprints = await Task.WhenAll(workflowDefinition.Activities.Select(async x => await CreateBlueprintsAsync(x, cancellationToken))); var activityBlueprints = manyActivityBlueprints.SelectMany(x => x).Distinct().ToDictionary(x => x.Id); - var compositeActivityBlueprints = activityBlueprints.Values.Where(x => x is ICompositeActivityBlueprint).Cast().ToList(); + var compositeActivityBlueprints = activityBlueprints.Values.Where(x => x is ICompositeActivityBlueprint).Cast().ToList(); var connections = compositeActivityBlueprints.SelectMany(x => x.Connections).Distinct().ToList(); var propertyProviders = compositeActivityBlueprints.SelectMany(x => x.ActivityPropertyProviders).ToList(); - + connections.AddRange(workflowDefinition.Connections.Select(x => ResolveConnection(x, activityBlueprints)).Where(x => x != null).Select(x => x!)); propertyProviders.AddRange(await CreatePropertyProviders(workflowDefinition, cancellationToken)); @@ -63,7 +67,7 @@ namespace Elsa.Services var activityType = await _activityTypeService.GetActivityTypeAsync(activityDefinition.Type, cancellationToken); var type = activityType.Type; var props = type.GetProperties(); - + foreach (var property in activityDefinition.Properties) { var prop = props.FirstOrDefault(x => x.Name == property.Name); @@ -73,7 +77,7 @@ namespace Elsa.Services _logger.LogWarning("Could not find the specified property '{PropertyName}' for activity type {ActivityTypeName}. Was the activity property renamed/removed/refactored after the workflow definition was created?", property.Name, activityType.Type.Name); continue; } - + var provider = new ExpressionActivityPropertyValueProvider(property.Expression, property.Syntax, prop.PropertyType); propertyProviders.AddProvider(activityDefinition.ActivityId, property.Name, provider); } @@ -101,12 +105,12 @@ namespace Elsa.Services private async Task> CreateBlueprintsAsync(ActivityDefinition activityDefinition, CancellationToken cancellationToken) { var list = new List(); - + if (activityDefinition is CompositeActivityDefinition compositeActivityDefinition) { var manyActivityBlueprints = await Task.WhenAll(compositeActivityDefinition.Activities.Select(async x => await CreateBlueprintsAsync(x, cancellationToken))); var activityBlueprints = manyActivityBlueprints.SelectMany(x => x).ToDictionary(x => x.Id); - + list.AddRange(activityBlueprints.Values); var compositeActivityBlueprint = new CompositeActivityBlueprint @@ -121,12 +125,12 @@ namespace Elsa.Services LoadWorkflowContext = activityDefinition.LoadWorkflowContext, SaveWorkflowContext = activityDefinition.SaveWorkflowContext, ActivityPropertyProviders = await CreatePropertyProviders(compositeActivityDefinition, cancellationToken) - }; - + }; + list.Add(compositeActivityBlueprint); - + // Connect the composite activity to its starting activities. - var startActivities = compositeActivityBlueprint.GetStartActivities().ToList(); + var startActivities = startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); compositeActivityBlueprint.Connections.AddRange(startActivities.Select(x => new Connection(compositeActivityBlueprint, x, CompositeActivity.Enter))); } else @@ -140,7 +144,7 @@ namespace Elsa.Services PersistWorkflow = activityDefinition.PersistWorkflow, LoadWorkflowContext = activityDefinition.LoadWorkflowContext, SaveWorkflowContext = activityDefinition.SaveWorkflowContext, - }); + }); } return list; diff --git a/src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs b/src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs new file mode 100644 index 000000000..e2f806c4a --- /dev/null +++ b/src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// Default implementation of . + /// + public class WorkflowExecutionContextForWorkflowBlueprintFactory : ICreatesWorkflowExecutionContextForWorkflowBlueprint + { + readonly IServiceProvider serviceProvider; + readonly IWorkflowFactory workflowFactory; + + public WorkflowExecutionContextForWorkflowBlueprintFactory(IServiceProvider serviceProvider, IWorkflowFactory workflowFactory) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.workflowFactory = workflowFactory ?? throw new ArgumentNullException(nameof(workflowFactory)); + } + + /// + /// Creates a workflow execution context for the specified workflow blueprint. + /// + /// A workflow blueprint + /// An optional cancellation token + /// A task for a workflow execution context + public async Task CreateWorkflowExecutionContextAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default) + { + var workflowInstance = await workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken); + return new WorkflowExecutionContext(serviceProvider, workflowBlueprint, workflowInstance); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowReviver.cs b/src/core/Elsa.Core/Services/WorkflowReviver.cs index 24dbb5d15..01b2ee519 100644 --- a/src/core/Elsa.Core/Services/WorkflowReviver.cs +++ b/src/core/Elsa.Core/Services/WorkflowReviver.cs @@ -14,13 +14,19 @@ namespace Elsa.Services private readonly IWorkflowQueue _workflowQueue; private readonly IWorkflowRegistry _workflowRegistry; private readonly IWorkflowInstanceStore _workflowInstanceStore; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public WorkflowReviver(IWorkflowRunner workflowRunner, IWorkflowQueue workflowQueue, IWorkflowRegistry workflowRegistry, IWorkflowInstanceStore workflowInstanceStore) + public WorkflowReviver(IWorkflowRunner workflowRunner, + IWorkflowQueue workflowQueue, + IWorkflowRegistry workflowRegistry, + IWorkflowInstanceStore workflowInstanceStore, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { _workflowRunner = workflowRunner; _workflowQueue = workflowQueue; _workflowRegistry = workflowRegistry; _workflowInstanceStore = workflowInstanceStore; + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); } public async Task ReviveAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) @@ -81,7 +87,7 @@ namespace Elsa.Services if (workflowBlueprint == null) throw new WorkflowException($"Could not find associated workflow definition {workflowInstance.DefinitionId} with version {workflowInstance.Version}"); - var startActivity = workflowBlueprint.GetStartActivities().FirstOrDefault(); + var startActivity = startingActivitiesProvider.GetStartActivities(workflowBlueprint).FirstOrDefault(); if (startActivity == null) throw new WorkflowException($"Cannot revive workflow {workflowInstance.Id} because it has no start activities"); diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index ca3ea1382..0d1cd4c9e 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -37,6 +37,7 @@ namespace Elsa.Services private readonly IMediator _mediator; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; public WorkflowRunner( IWorkflowRegistry workflowRegistry, @@ -48,7 +49,8 @@ namespace Elsa.Services Func workflowBuilderFactory, IMediator mediator, IServiceScopeFactory serviceScopeFactory, - ILogger logger) + ILogger logger, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { _workflowRegistry = workflowRegistry; _workflowFactory = workflowFactory; @@ -56,6 +58,7 @@ namespace Elsa.Services _mediator = mediator; _serviceScopeFactory = serviceScopeFactory; _logger = logger; + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); _workflowInstanceManager = workflowInstanceStore; _workflowContextManager = workflowContextManager; _bookmarkFinder = bookmarkFinder; @@ -284,7 +287,7 @@ namespace Elsa.Services private async Task BeginWorkflow(WorkflowExecutionContext workflowExecutionContext, IActivityBlueprint? activity, object? input, CancellationToken cancellationToken) { if (activity == null) - activity = workflowExecutionContext.WorkflowBlueprint.GetStartActivities().FirstOrDefault() ?? workflowExecutionContext.WorkflowBlueprint.Activities.First(); + activity = startingActivitiesProvider.GetStartActivities(workflowExecutionContext.WorkflowBlueprint).FirstOrDefault() ?? workflowExecutionContext.WorkflowBlueprint.Activities.First(); if (!await CanExecuteAsync(workflowExecutionContext, activity, input, false, cancellationToken)) return false; diff --git a/src/core/Elsa.Core/Triggers/TriggerIndexer.cs b/src/core/Elsa.Core/Triggers/TriggerIndexer.cs index b3a4d368a..3bd7d50ec 100644 --- a/src/core/Elsa.Core/Triggers/TriggerIndexer.cs +++ b/src/core/Elsa.Core/Triggers/TriggerIndexer.cs @@ -17,36 +17,24 @@ namespace Elsa.Triggers public class TriggerIndexer : ITriggerIndexer { private readonly IWorkflowRegistry _workflowRegistry; - private readonly IBookmarkHasher _bookmarkHasher; - private readonly IEnumerable _providers; - private readonly IServiceProvider _serviceProvider; - private readonly IWorkflowFactory _workflowFactory; - private readonly IActivityTypeService _activityTypeService; private readonly ITriggerStore _triggerStore; private readonly IMediator _mediator; private readonly ILogger _logger; private readonly Stopwatch _stopwatch = new(); + private readonly IGetsTriggersForWorkflowBlueprints _triggersForBookmarksProvider; public TriggerIndexer( IWorkflowRegistry workflowRegistry, - IBookmarkHasher bookmarkHasher, - IEnumerable providers, - IServiceProvider serviceProvider, - IWorkflowFactory workflowFactory, - IActivityTypeService activityTypeService, ITriggerStore triggerStore, IMediator mediator, - ILogger logger) + ILogger logger, + IGetsTriggersForWorkflowBlueprints triggersForBookmarksProvider) { _workflowRegistry = workflowRegistry; - _bookmarkHasher = bookmarkHasher; - _providers = providers; - _serviceProvider = serviceProvider; - _workflowFactory = workflowFactory; - _activityTypeService = activityTypeService; _triggerStore = triggerStore; _mediator = mediator; _logger = logger; + _triggersForBookmarksProvider = triggersForBookmarksProvider; } public async Task IndexTriggersAsync(CancellationToken cancellationToken = default) @@ -63,49 +51,12 @@ namespace Elsa.Triggers _logger.LogInformation("Indexing triggers"); var workflowBlueprintList = workflowBlueprints.ToList(); - var triggers = (await GetTriggersAsync(workflowBlueprintList, cancellationToken)).ToList(); + var triggers = (await _triggersForBookmarksProvider.GetTriggersAsync(workflowBlueprintList, cancellationToken)).ToList(); _stopwatch.Stop(); _logger.LogInformation("Indexed {TriggerCount} triggers in {ElapsedTime}", triggers.Count, _stopwatch.Elapsed); await _triggerStore.StoreAsync(triggers, cancellationToken); } - - private async Task> GetTriggersAsync(ICollection workflowBlueprints, CancellationToken cancellationToken) - { - var allTriggers = new List(); - var activityTypes = (await _activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); - - foreach (var workflowBlueprint in workflowBlueprints) - { - var startActivities = workflowBlueprint.GetStartActivities(); - var workflowInstance = await _workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken); - var workflowExecutionContext = new WorkflowExecutionContext(_serviceProvider, workflowBlueprint, workflowInstance); - - foreach (var activity in startActivities) - { - var activityExecutionContext = new ActivityExecutionContext(_serviceProvider, workflowExecutionContext, activity, null, false, cancellationToken); - var activityType = activityTypes[activity.Type]; - var context = new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); - var providers = await FilterProvidersAsync(context).ToListAsync(cancellationToken); - - foreach (var provider in providers) - { - var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); - var triggers = bookmarks.Select(x => new WorkflowTrigger(workflowBlueprint, activity.Id, activity.Type, _bookmarkHasher.Hash(x), x)).ToList(); - allTriggers.AddRange(triggers); - } - } - } - - return allTriggers; - } - - private async IAsyncEnumerable FilterProvidersAsync(BookmarkProviderContext context) - { - foreach (var provider in _providers) - if (await provider.SupportsActivityAsync(context)) - yield return provider; - } } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs new file mode 100644 index 000000000..6769d3136 --- /dev/null +++ b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// Default implementation of . + /// + public class TriggersForActivityBlueprintAndWorkflowProvider : IGetsTriggersForActivityBlueprintAndWorkflow + { + readonly IBookmarkHasher bookmarkHasher; + readonly IEnumerable bookmarkProviders; + readonly ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory; + + public TriggersForActivityBlueprintAndWorkflowProvider(IBookmarkHasher bookmarkHasher, + IEnumerable bookmarkProviders, + ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory) + { + this.bookmarkHasher = bookmarkHasher ?? throw new System.ArgumentNullException(nameof(bookmarkHasher)); + this.bookmarkProviders = bookmarkProviders ?? throw new System.ArgumentNullException(nameof(bookmarkProviders)); + this.activityExecutionContextFactory = activityExecutionContextFactory ?? throw new System.ArgumentNullException(nameof(activityExecutionContextFactory)); + } + + /// + /// Gets a collection of the workflow triggers for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A dictionary of all of the activity types (by name) + /// An optional cancellation token + /// A task exposing a collection of workflow triggers for the activity and workflow. + public async Task> GetTriggersForActivityBlueprintAsync(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + IDictionary activityTypes, + CancellationToken cancellationToken = default) + { + var bookmarkProviderContext = GetBookmarkProviderContext(activityBlueprint, workflowExecutionContext, cancellationToken, activityTypes); + var supportedBookmarkProviders = await GetSupportedBookmarkProvidersForContextAsync(bookmarkProviderContext) + .ToListAsync(cancellationToken); + + var tasksOfListsOfTriggers = supportedBookmarkProviders + .Select(bookmarkProvider => GetTriggersForBookmarkProvider(bookmarkProvider, + bookmarkProviderContext, + activityBlueprint, + workflowExecutionContext.WorkflowBlueprint, + cancellationToken)); + return (await Task.WhenAll(tasksOfListsOfTriggers)) + .SelectMany(x => x) + .ToList(); + } + + BookmarkProviderContext GetBookmarkProviderContext(IActivityBlueprint activity, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken, + IDictionary activityTypes) + { + var activityExecutionContext = activityExecutionContextFactory.CreateActivityExecutionContext(activity, + workflowExecutionContext, + cancellationToken); + var activityType = activityTypes[activity.Type]; + return new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); + } + + async IAsyncEnumerable GetSupportedBookmarkProvidersForContextAsync(BookmarkProviderContext context) + { + foreach (var provider in bookmarkProviders) + if (await provider.SupportsActivityAsync(context)) + yield return provider; + } + + async Task> GetTriggersForBookmarkProvider(IBookmarkProvider provider, + BookmarkProviderContext context, + IActivityBlueprint activityBlueprint, + IWorkflowBlueprint workflowBlueprint, + CancellationToken cancellationToken = default) + { + var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); + return bookmarks + .Select(x => new WorkflowTrigger(workflowBlueprint, activityBlueprint.Id, activityBlueprint.Type, bookmarkHasher.Hash(x), x)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs new file mode 100644 index 000000000..0b57b66c3 --- /dev/null +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// Default implementation of which + /// gets all of the workflow triggers for a collection of workflow blueprints. + /// + public class TriggersForBlueprintsProvider : IGetsTriggersForWorkflowBlueprints + { + readonly IActivityTypeService activityTypeService; + readonly ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory; + readonly IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider; + readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; + + public TriggersForBlueprintsProvider(IActivityTypeService activityTypeService, + ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, + IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) + { + this.activityTypeService = activityTypeService ?? throw new ArgumentNullException(nameof(activityTypeService)); + this.workflowExecutionContextFactory = workflowExecutionContextFactory ?? throw new ArgumentNullException(nameof(workflowExecutionContextFactory)); + this.triggerProvider = triggerProvider ?? throw new ArgumentNullException(nameof(triggerProvider)); + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); + } + + /// + /// Gets the triggers for all of the specified workflow blueprints. + /// + /// The workflow blueprints for which to get triggers. + /// An optional cancellation token. + /// A task which exposes an enumerable collection of workflow triggers. + public async Task> GetTriggersAsync(IEnumerable workflowBlueprints, + CancellationToken cancellationToken = default) + { + var activityTypes = (await activityTypeService.GetActivityTypesAsync(cancellationToken)) + .ToDictionary(x => x.TypeName); + + var tasksOfListsOfTriggers = workflowBlueprints + .Select(workflowBlueprint => GetWorkflowTriggersForWorkflowBlueprintAsync(workflowBlueprint, activityTypes, cancellationToken)); + + return (await Task.WhenAll(tasksOfListsOfTriggers)) + .SelectMany(x => x) + .ToList(); + } + + async Task> GetWorkflowTriggersForWorkflowBlueprintAsync(IWorkflowBlueprint workflowBlueprint, + IDictionary activityTypes, + CancellationToken cancellationToken) + { + var startingActivityBlueprints = startingActivitiesProvider.GetStartActivities(workflowBlueprint); + var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, cancellationToken); + var tasksOfCollectionsOfTriggers = startingActivityBlueprints + .Select(async activityBlueprint => await triggerProvider.GetTriggersForActivityBlueprintAsync(activityBlueprint, + workflowExecutionContext, + activityTypes, + cancellationToken)); + return (await Task.WhenAll(tasksOfCollectionsOfTriggers)) + .SelectMany(x => x) + .ToList(); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs index 48ef3dddc..c0aa02255 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs @@ -11,6 +11,7 @@ using Elsa.Persistence.EntityFramework.Sqlite; using Elsa.Persistence.YesSql; using YesSql.Provider.Sqlite; using System.Data; +using Elsa.Testing.Shared.Helpers; namespace Elsa.Core.IntegrationTests.Autofixture { @@ -48,13 +49,15 @@ namespace Elsa.Core.IntegrationTests.Autofixture { public override ICustomization GetCustomization(ParameterInfo parameter) { + var tempFolder = new TemporaryFolder(); + return new HostBubilderUsingServicesCustomization(services => { services .AddElsa(elsa => { elsa .AddWorkflow() .UseEntityFrameworkPersistence(opts => { - opts.UseSqlite("Data Source=elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); + opts.UseSqlite($"Data Source={tempFolder.Folder}elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); }); }); }, parameter); diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs index 4fd62d32a..ebf0974c7 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Elsa.Persistence.EntityFramework.Core.Extensions; using Microsoft.EntityFrameworkCore; using Elsa.Persistence.EntityFramework.Sqlite; +using Elsa.Testing.Shared.Helpers; namespace Elsa.Core.IntegrationTests.Autofixture { @@ -13,12 +14,14 @@ namespace Elsa.Core.IntegrationTests.Autofixture { public override ICustomization GetCustomization(ParameterInfo parameter) { + var tempFolder = new TemporaryFolder(); + return new HostBubilderUsingServicesCustomization(services => { services .AddElsa(elsa => { elsa .UseEntityFrameworkPersistence(opts => { - opts.UseSqlite("Data Source=elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); + opts.UseSqlite($"Data Source={tempFolder.Folder}elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); }) .AddPersistableWorkflow(); }); diff --git a/test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs new file mode 100644 index 000000000..d782f0b91 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Primitives; +using Elsa.Activities.Signaling; +using Elsa.Activities.UserTask.Activities; +using Elsa.Builders; +using Elsa.Models; +using Elsa.Persistence; +using Elsa.Triggers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Elsa.Core.IntegrationTests.Triggers +{ + public class TriggerIndexerIntegrationTests + { + [Fact(DisplayName = "The IndexTriggersAsync method should return a trigger for a blocking start activity")] + public async Task IndexTriggersAsyncShouldIncludeBlockingStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.True(allTriggers.Any(x => x.ActivityType == nameof(SignalReceived) + && x.WorkflowBlueprint.Name == nameof(WorkflowWithBlockingStartActivity)), + "A trigger exists for the expected blocking activity"); + } + + [Fact(DisplayName = "The IndexTriggersAsync method should not return a trigger for a non-blocking start activity")] + public async Task IndexTriggersAsyncShouldNotIncludeNonBlockingStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.False(allTriggers.Any(x => x.WorkflowBlueprint.Name == nameof(WorkflowWithNonBlockingStartActivity)), + "No triggers exist for the workflow which starts with a non-blocking activity"); + } + + [Fact(DisplayName = "The IndexTriggersAsync method should return a trigger for a blocking start activity within a composite activity", + Skip = "This test is the repro case for #738, which is not yet implemented")] + public async Task IndexTriggersAsyncShouldIncludeBlockingCompositeStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.True(allTriggers.Any(x => x.WorkflowBlueprint.Name == "WorkflowWithBlockingCompositeStartActivity"), + "A trigger exists for the expected blocking composite activity"); + } + + [Fact(DisplayName = "The IndexTriggersAsync method should not return a trigger for a non-blocking start activity within a composite activity")] + public async Task IndexTriggersAsyncShouldNotIncludeNonBlockingCompositeStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.False(allTriggers.Any(x => x.WorkflowBlueprint.Name == "WorkflowWithNonBlockingCompositeStartActivity"), + "No triggers exist for the workflow which starts with a non-blocking composite activity"); + } + + async Task> IndexThenGetAllTriggersAsync() + { + var serviceProvider = await GetServiceProvider(); + + var sut = serviceProvider.GetRequiredService(); + await sut.IndexTriggersAsync(); + + var triggerStore = serviceProvider.GetRequiredService(); + return await triggerStore.GetAsync(); + } + + async Task GetServiceProvider() + { + var services = new ServiceCollection(); + services.AddElsa(elsa => { + elsa + .AddWorkflow() + .AddWorkflow() + .AddActivity() + .AddActivity() + ; + }); + + var serviceProvider = services.BuildServiceProvider(); + + var definitionStore = serviceProvider.GetRequiredService(); + await definitionStore.AddAsync(new WorkflowDefinition + { + Id = "1", + DefinitionId = "WorkflowWithNonBlockingCompositeStartActivity", + Name = "WorkflowWithNonBlockingCompositeStartActivity", + Version = 1, + IsPublished = true, + IsLatest = true, + PersistenceBehavior = WorkflowPersistenceBehavior.Suspended, + Activities = new[] + { + GetNonBlockingCompositeActivityDefinition("nonBlockingComposite1"), + GetNonBlockingActivityDefinition("nonBlocking1"), + }, + Connections = new[] + { + new ConnectionDefinition("nonBlockingComposite1", "nonBlocking1", OutcomeNames.Done), + } + }); + await definitionStore.AddAsync(new WorkflowDefinition + { + Id = "2", + DefinitionId = "WorkflowWithBlockingCompositeStartActivity", + Name = "WorkflowWithBlockingCompositeStartActivity", + Version = 1, + IsPublished = true, + IsLatest = true, + PersistenceBehavior = WorkflowPersistenceBehavior.Suspended, + Activities = new[] + { + GetBlockingCompositeActivityDefinition("blockingComposite1"), + GetNonBlockingActivityDefinition("nonBlocking2"), + }, + Connections = new[] + { + new ConnectionDefinition("blockingComposite1", "nonBlocking2", OutcomeNames.Done), + } + }); + + return serviceProvider; + } + + class WorkflowWithBlockingStartActivity : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(a => a.Set(x => x.Signal, "MySignal").Set(x => x.Id, "SignalReceived1")) + .Then(a => a.Set(x => x.Id, "Finish1")); + } + } + + class WorkflowWithNonBlockingStartActivity : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(t => t.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused").Set(x => x.Id, "SetVariable1")) + .Then(a => a.Set(x => x.Id, "Finish2")); + } + } + + ActivityDefinition GetBlockingActivityDefinition(string id) + { + return new() + { + ActivityId = id, + Type = nameof(SignalReceived), + Properties = new [] + { + ActivityDefinitionProperty.Literal(nameof(SignalReceived.Signal), "MySignal"), + } + }; + } + + ActivityDefinition GetNonBlockingActivityDefinition(string id) + { + return new() + { + ActivityId = id, + Type = nameof(SetVariable), + Properties = new [] + { + ActivityDefinitionProperty.Literal(nameof(SetVariable.VariableName), "Unused"), + ActivityDefinitionProperty.Literal(nameof(SetVariable.Value), "Unused"), + } + }; + } + + ActivityDefinition GetBlockingCompositeActivityDefinition(string id) + { + return new CompositeActivityDefinition + { + ActivityId = id, + Activities = new [] + { + GetBlockingActivityDefinition("SignalReceived2"), + GetNonBlockingActivityDefinition("SetVariable2"), + }, + Connections = new [] + { + new ConnectionDefinition("UserTask2", "SetVariable2", OutcomeNames.Done), + } + }; + } + + ActivityDefinition GetNonBlockingCompositeActivityDefinition(string id) + { + return new CompositeActivityDefinition + { + ActivityId = id, + Activities = new [] + { + GetNonBlockingActivityDefinition("SetVariable3"), + GetNonBlockingActivityDefinition("SetVariable4"), + }, + Connections = new [] + { + new ConnectionDefinition("SetVariable3", "SetVariable4", OutcomeNames.Done), + } + }; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs new file mode 100644 index 000000000..ae8a3187c --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.Customizations; + +namespace Elsa.Testing.Shared.AutoFixture.Attributes +{ + public class MockBookmarkProvidersAttribute : CustomizeAttribute + { + readonly int howMany; + + public override ICustomization GetCustomization(ParameterInfo parameter) + => new MockBookmarkProvidersCustomization(parameter, howMany); + + public MockBookmarkProvidersAttribute(int howMany = 3) + { + this.howMany = howMany; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs new file mode 100644 index 000000000..30d09bf43 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using AutoFixture.Kernel; +using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders; + +namespace Elsa.Testing.Shared.AutoFixture.Customizations +{ + public class MockBookmarkProvidersCustomization : SpecimenBuilderForParameterCustomization + { + readonly int howMany; + + protected override ISpecimenBuilder GetUnfilteredSpecimenBuilder() + => new MockBookmarkProvidersSpecimenBuilder(howMany); + + public MockBookmarkProvidersCustomization(ParameterInfo parameter, int howMany) : base(parameter) + { + this.howMany = howMany; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs new file mode 100644 index 000000000..83bff4b37 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using AutoFixture.Kernel; +using Elsa.Bookmarks; +using Moq; + +namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders +{ + public class MockBookmarkProvidersSpecimenBuilder : ISpecimenBuilder + { + readonly int howMany; + + public object Create(object request, ISpecimenContext context) + { + if (!request.IsAnAutofixtureRequestForType>()) + return new NoSpecimen(); + + return Enumerable.Range(0, howMany) + .Select(x => Mock.Of()) + .ToList(); + } + + public MockBookmarkProvidersSpecimenBuilder(int howMany) + { + this.howMany = howMany; + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs b/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs new file mode 100644 index 000000000..8949c53a0 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Xunit; + +namespace Elsa.Services +{ + public class ActivityExecutionContextForActivityBlueprintFactoryTests + { + [Theory(DisplayName = "The CreateActivityExecutionContext method should create a context using the activity blueprint, the workflow execution context, cancellation token and injected service provider."), AutoMoqData] + public void CreateActivityExecutionContextCreatesContextUsingBlueprintExecutionContextCancellationTokenAndServiceProvider([AutofixtureServiceProvider] IServiceProvider serviceProvider, + IActivityBlueprint activityBlueprint, + [OmitOnRecursion] WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) + { + var sut = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); + + var result = sut.CreateActivityExecutionContext(activityBlueprint, workflowExecutionContext, cancellationToken); + + Assert.True(ReferenceEquals(activityBlueprint, result.ActivityBlueprint), "The activity blueprint should be the same"); + Assert.True(ReferenceEquals(workflowExecutionContext, result.WorkflowExecutionContext), "The workflow execution context should be the same"); + Assert.True(Equals(cancellationToken, result.CancellationToken), "The cancellation token should be equal"); + Assert.True(ReferenceEquals(serviceProvider, result.ServiceProvider), "The service provider should be the same"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs b/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs new file mode 100644 index 000000000..eb421affa --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using Elsa.Services.Models; +using Moq; +using Xunit; + +namespace Elsa.Services +{ + public class StartActivitiesForCompositeActivityBlueprintProviderTests + { + [Theory(DisplayName = "The GetStartActivities returns only activities that have no inbound workflow connections"), AutoMoqData] + public void GetStartActivitiesReturnsAllActivitiesWhichHaveNoInboundConnections(StartActivitiesForCompositeActivityBlueprintProvider sut, + IWorkflowBlueprint workflowBlueprint, + IActivityBlueprint activityBlueprint1, + IActivityBlueprint activityBlueprint2, + IActivityBlueprint activityBlueprint3, + IActivityBlueprint activityBlueprint4, + string activityBlueprintId1, + string activityBlueprintId2, + string activityBlueprintId3, + string activityBlueprintId4, + IConnection connection1, + IConnection connection2, + ITargetEndpoint endpoint1, + ITargetEndpoint endpoint2) + { + SetupActivitiesWithIds(new () { + {activityBlueprintId1, activityBlueprint1}, + {activityBlueprintId2, activityBlueprint2}, + {activityBlueprintId3, activityBlueprint3}, + {activityBlueprintId4, activityBlueprint4}, + }); + Mock.Get(workflowBlueprint).SetupGet(x => x.Connections).Returns(new [] { connection1, connection2 }); + Mock.Get(workflowBlueprint).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint3, activityBlueprint4 }); + Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); + Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); + Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint2); + Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint4); + + var result = sut.GetStartActivities(workflowBlueprint).ToArray(); + + Assert.Equal(new [] { activityBlueprint1, activityBlueprint3 }, result); + } + + void SetupActivitiesWithIds(Dictionary idsToBlueprints) + { + foreach(var kvp in idsToBlueprints) + Mock.Get(kvp.Value).SetupGet(x => x.Id).Returns(kvp.Key); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs b/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs new file mode 100644 index 000000000..20ba00b63 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Elsa.Models; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Moq; +using Xunit; + +namespace Elsa.Services +{ + public class WorkflowExecutionContextForWorkflowBlueprintFactoryTests + { + [Theory(DisplayName = "The CreateWorkflowExecutionContextAsync method returns an execution context using the blueprint, an instance and the service provider"), AutoMoqData] + public async Task CreateWorkflowExecutionContextAsyncReturnsContextWithBlueprintInstanceAndServiceProvider([AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowFactory workflowFactory, + IWorkflowBlueprint workflowBlueprint, + [OmitOnRecursion] WorkflowInstance instance) + { + var sut = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); + Mock.Get(workflowFactory) + .Setup(x => x.InstantiateAsync(workflowBlueprint, default, default, default)) + .Returns(() => Task.FromResult(instance)); + + var result = await sut.CreateWorkflowExecutionContextAsync(workflowBlueprint); + + Assert.Same(serviceProvider, result.ServiceProvider); + Assert.Same(workflowBlueprint, result.WorkflowBlueprint); + Assert.Same(instance, result.WorkflowInstance); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs new file mode 100644 index 000000000..22a0c1d69 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Models; +using Elsa.Services; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Moq; +using Xunit; + +namespace Elsa.Triggers +{ + public class TriggersForActivityBlueprintAndWorkflowProviderTests + { + [Theory(DisplayName = "The GetTriggersForActivityBlueprintAsync returns a trigger for every bookmark in the bookmark providers that support the activity"), AutoMoqData] + public async Task GetTriggersForActivityBlueprintAsyncReturnsTriggersForEachBookmarkInSupportedBookmarkProviders(IBookmarkHasher bookmarkHasher, + IBookmarkProvider bookmarkProvider1, + IBookmarkProvider unsupportedBookmarkProvider, + IBookmarkProvider bookmarkProvider2, + ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory, + IActivityBlueprint activityBlueprint, + [AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowBlueprint workflowBlueprint, + [OmitOnRecursion] WorkflowInstance workflowInstance, + ActivityType activityType, + IBookmark bookmark1, + IBookmark bookmark2, + IBookmark bookmark3, + IBookmark bookmark4) + { + var sut = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, + new[] { bookmarkProvider1, unsupportedBookmarkProvider, bookmarkProvider2 }, + activityExecutionContextFactory); + var workflowExecutionContext = new WorkflowExecutionContext(serviceProvider, workflowBlueprint, workflowInstance); + var activityExecutionContext = new ActivityExecutionContext(serviceProvider, workflowExecutionContext, activityBlueprint, default, default, default); + + Mock.Get(activityExecutionContextFactory) + .Setup(x => x.CreateActivityExecutionContext(activityBlueprint, workflowExecutionContext, default)) + .Returns(activityExecutionContext); + Mock.Get(activityBlueprint).SetupGet(x => x.Type).Returns(activityType.TypeName); + Mock.Get(bookmarkProvider1) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(true)); + Mock.Get(unsupportedBookmarkProvider) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(false)); + Mock.Get(bookmarkProvider2) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(true)); + Mock.Get(bookmarkProvider1) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(() => ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); + Mock.Get(bookmarkProvider2) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(() => ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); + + var result = await sut.GetTriggersForActivityBlueprintAsync(activityBlueprint, + workflowExecutionContext, + new Dictionary { { activityType.TypeName, activityType } }); + + Assert.True(result.Any(x => x.Bookmark == bookmark1), "Result contains a trigger for bookmark 1"); + Assert.True(result.Any(x => x.Bookmark == bookmark2), "Result contains a trigger for bookmark 2"); + Assert.True(result.Any(x => x.Bookmark == bookmark3), "Result contains a trigger for bookmark 3"); + Assert.True(result.Any(x => x.Bookmark == bookmark4), "Result contains a trigger for bookmark 4"); + Assert.True(result.Count() == 4, "Result has 4 items"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs new file mode 100644 index 000000000..4ed32364e --- /dev/null +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture.Xunit2; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Models; +using Elsa.Services; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Elsa.Triggers; +using Moq; +using Xunit; + +namespace Elsa.UnitTests.Triggers +{ + public class TriggersForBlueprintsProviderTests + { + [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for start activities of the workflow blueprints"), AutoMoqData] + public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks([Frozen] IActivityTypeService activityTypeService, + [Frozen] ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, + [Frozen] IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider, + [Frozen] IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider, + TriggersForBlueprintsProvider sut, + IWorkflowBlueprint workflowBlueprint1, + IWorkflowBlueprint workflowBlueprint2, + ActivityType activityType1, + ActivityType activityType2, + ActivityType activityType3, + IActivityBlueprint activityBlueprint1, + IActivityBlueprint activityBlueprint2, + IActivityBlueprint activityBlueprint3, + WorkflowTrigger trigger1, + WorkflowTrigger trigger2, + WorkflowTrigger trigger3, + WorkflowTrigger trigger4, + WorkflowTrigger trigger5, + WorkflowTrigger trigger6, + [AutofixtureServiceProvider] IServiceProvider serviceProvider, + [NoAutoProperties] WorkflowInstance workflowInstance) + { + Mock.Get(activityTypeService) + .Setup(x => x.GetActivityTypesAsync(default)) + .Returns(ValueTask.FromResult>(new [] { activityType1, activityType2, activityType3 })); + Mock.Get(startingActivitiesProvider) + .Setup(x => x.GetStartActivities(workflowBlueprint1)) + .Returns(() => new [] { activityBlueprint1, activityBlueprint2 }); + Mock.Get(startingActivitiesProvider) + .Setup(x => x.GetStartActivities(workflowBlueprint2)) + .Returns(() => new [] { activityBlueprint3 }); + Mock.Get(workflowExecutionContextFactory) + .Setup(x => x.CreateWorkflowExecutionContextAsync(It.IsAny(), default)) + .Returns((IWorkflowBlueprint bp, CancellationToken c) => Task.FromResult(new WorkflowExecutionContext(serviceProvider, bp, workflowInstance, default))); + Mock.Get(triggerProvider) + .Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint1, It.IsAny(), It.IsAny>(), default)) + .Returns(() => Task.FromResult>(new [] { trigger1, trigger2 })); + Mock.Get(triggerProvider) + .Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint2, It.IsAny(), It.IsAny>(), default)) + .Returns(() => Task.FromResult>(new [] { trigger3, trigger4 })); + Mock.Get(triggerProvider) + .Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint3, It.IsAny(), It.IsAny>(), default)) + .Returns(() => Task.FromResult>(new [] { trigger5, trigger6 })); + + var results = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); + + Assert.Equal(new [] { trigger1, trigger2, trigger3, trigger4, trigger5, trigger6 }, + results); + } + } +} \ No newline at end of file