Merge pull request #775 from craigfowler/feature/738-Composite-Activities-may-be-triggers

Refactor for comprehension & add test coverage
This commit is contained in:
Craig Fowler 2021-03-20 11:29:28 +00:00 committed by GitHub
commit ee7a8cdb1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 989 additions and 93 deletions

View file

@ -9,21 +9,6 @@ namespace Elsa
{
public static class CompositeActivityBlueprintExtensions
{
public static IEnumerable<IActivityBlueprint> 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<IActivityBlueprint> GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint, string activityType) => workflowBlueprint.GetStartActivities().Where(x => x.Type == activityType);
public static IEnumerable<IActivityBlueprint> GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint, Type activityType) => workflowBlueprint.GetStartActivities(activityType.Name);
public static IEnumerable<IActivityBlueprint> GetStartActivities<T>(this ICompositeActivityBlueprint workflowBlueprint) where T : IActivity => workflowBlueprint.GetStartActivities(typeof(T));
public static IEnumerable<IActivityBlueprint> 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<IActivityBlueprint> GetActivities(this ICompositeActivityBlueprint workflowBlueprint, IEnumerable<string> ids) => workflowBlueprint.Activities.Where(x => ids.Contains(x.Id));

View file

@ -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<IActivityBlueprint> GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider,
ICompositeActivityBlueprint workflowBlueprint,
string activityType)
=> startActivitiesProvider.GetStartActivities(workflowBlueprint).Where(x => x.Type == activityType);
public static IEnumerable<IActivityBlueprint> GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider,
ICompositeActivityBlueprint workflowBlueprint,
Type activityType)
=> startActivitiesProvider.GetStartActivities(workflowBlueprint, activityType.Name);
public static IEnumerable<IActivityBlueprint> GetStartActivities<T>(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider,
ICompositeActivityBlueprint workflowBlueprint) where T : IActivity
=> startActivitiesProvider.GetStartActivities(workflowBlueprint, typeof(T));
}
}

View file

@ -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<ActivityDefinition> GetStartActivities(this WorkflowDefinition workflowDefinition)
{
var targetActivities = workflowDefinition.Connections

View file

@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// An object which can create an activity execution context for a specified activity blueprint.
/// </summary>
public interface ICreatesActivityExecutionContextForActivityBlueprint
{
/// <summary>
/// Creates a activity execution context for the specified activity blueprint.
/// </summary>
/// <param name="activityBlueprint">An activity blueprint</param>
/// <param name="workflowExecutionContext">A workflow execution context</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>An activity execution context</returns>
ActivityExecutionContext CreateActivityExecutionContext(IActivityBlueprint activityBlueprint,
WorkflowExecutionContext workflowExecutionContext,
CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// An object which can create a workflow execution context for a specified workflow blueprint.
/// </summary>
public interface ICreatesWorkflowExecutionContextForWorkflowBlueprint
{
/// <summary>
/// Creates a workflow execution context for the specified workflow blueprint.
/// </summary>
/// <param name="workflowBlueprint">A workflow blueprint</param>
/// <param name="cancellationToken">An optional cancellation token</param>
/// <returns>A task for a workflow execution context</returns>
Task<WorkflowExecutionContext> CreateWorkflowExecutionContextAsync(IWorkflowBlueprint workflowBlueprint,
CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// An object which gets the starting activities for a specified <see cref="ICompositeActivityBlueprint"/>.
/// </summary>
public interface IGetsStartActivitiesForCompositeActivityBlueprint
{
/// <summary>
/// Gets a collection of the starting activities for the specified composite activity blueprint.
/// </summary>
/// <param name="compositeActivityBlueprint">A composite activity blueprint</param>
/// <returns>A collection of the blueprint's starting activities</returns>
IEnumerable<IActivityBlueprint> GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint);
}
}

View file

@ -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
{
/// <summary>
/// An object which can get a collection of the <see cref="WorkflowTrigger"/> for a specified activity blueprint and workflow.
/// </summary>
public interface IGetsTriggersForActivityBlueprintAndWorkflow
{
/// <summary>
/// Gets a collection of the workflow triggers for the specified activity blueprint.
/// </summary>
/// <param name="activityBlueprint">An activity blueprint</param>
/// <param name="workflowExecutionContext">A workflow execution context</param>
/// <param name="activityTypes">A dictionary of all of the activity types (by name)</param>
/// <param name="cancellationToken">An optional cancellation token</param>
/// <returns>A task exposing a collection of workflow triggers for the activity and workflow.</returns>
Task<IEnumerable<WorkflowTrigger>> GetTriggersForActivityBlueprintAsync(IActivityBlueprint activityBlueprint,
WorkflowExecutionContext workflowExecutionContext,
IDictionary<string, ActivityType> activityTypes,
CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Services.Models;
namespace Elsa.Triggers
{
/// <summary>
/// An object which can get all of the workflow triggers for a collection of workflow blueprints.
/// </summary>
public interface IGetsTriggersForWorkflowBlueprints
{
/// <summary>
/// Gets the triggers for all of the specified workflow blueprints.
/// </summary>
/// <param name="workflowBlueprints">The workflow blueprints for which to get triggers.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>A task which exposes an enumerable collection of workflow triggers.</returns>
Task<IEnumerable<WorkflowTrigger>> GetTriggersAsync(IEnumerable<IWorkflowBlueprint> workflowBlueprints,
CancellationToken cancellationToken = default);
}
}

View file

@ -14,9 +14,12 @@ namespace Elsa.Builders
public class CompositeActivityBuilder : ActivityBuilder, ICompositeActivityBuilder
{
private readonly Func<ICompositeActivityBuilder> _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<IActivityBuilder>();
ConnectionBuilders = new List<IConnectionBuilder>();
@ -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<T>(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)));
}
}

View file

@ -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();

View file

@ -109,6 +109,9 @@ namespace Microsoft.Extensions.DependencyInjection
.AddTransient<IActivityTypeService, ActivityTypeService>()
.AddActivityTypeProvider<TypeBasedActivityProvider>()
.AddScoped<IWorkflowExecutionLog, WorkflowExecutionLog>()
.AddTransient<ICreatesWorkflowExecutionContextForWorkflowBlueprint, WorkflowExecutionContextForWorkflowBlueprintFactory>()
.AddTransient<ICreatesActivityExecutionContextForActivityBlueprint, ActivityExecutionContextForActivityBlueprintFactory>()
.AddTransient<IGetsStartActivitiesForCompositeActivityBlueprint, StartActivitiesForCompositeActivityBlueprintProvider>()
;
// Serialization.
@ -143,6 +146,8 @@ namespace Microsoft.Extensions.DependencyInjection
.AddScoped<IBookmarkIndexer, BookmarkIndexer>()
.AddScoped<IBookmarkFinder, BookmarkFinder>()
.AddScoped<ITriggerIndexer, TriggerIndexer>()
.AddScoped<IGetsTriggersForWorkflowBlueprints, TriggersForBlueprintsProvider>()
.AddTransient<IGetsTriggersForActivityBlueprintAndWorkflow, TriggersForActivityBlueprintAndWorkflowProvider>()
.AddSingleton<ITriggerStore, TriggerStore>()
.AddScoped<ITriggerFinder, TriggerFinder>()
.AddBookmarkProvider<SignalReceivedBookmarkProvider>()

View file

@ -0,0 +1,33 @@
using System;
using System.Threading;
using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// Default implementation of <see cref="ICreatesActivityExecutionContextForActivityBlueprint"/>.
/// </summary>
public class ActivityExecutionContextForActivityBlueprintFactory : ICreatesActivityExecutionContextForActivityBlueprint
{
readonly IServiceProvider serviceProvider;
public ActivityExecutionContextForActivityBlueprintFactory(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
/// <summary>
/// Creates a activity execution context for the specified activity blueprint.
/// </summary>
/// <param name="activityBlueprint">An activity blueprint</param>
/// <param name="workflowExecutionContext">A workflow execution context</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>An activity execution context</returns>
public ActivityExecutionContext CreateActivityExecutionContext(IActivityBlueprint activityBlueprint,
WorkflowExecutionContext workflowExecutionContext,
CancellationToken cancellationToken)
{
return new ActivityExecutionContext(serviceProvider, workflowExecutionContext, activityBlueprint, null, false, cancellationToken);
}
}
}

View file

@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// Default implementation of <see cref="IGetsStartActivitiesForCompositeActivityBlueprint"/>.
/// </summary>
public class StartActivitiesForCompositeActivityBlueprintProvider : IGetsStartActivitiesForCompositeActivityBlueprint
{
/// <summary>
/// Gets a collection of the starting activities for the specified composite activity blueprint.
/// </summary>
/// <param name="compositeActivityBlueprint">A composite activity blueprint</param>
/// <returns>A collection of the blueprint's starting activities</returns>
public IEnumerable<IActivityBlueprint> GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint)
{
var activityIdsThatAreNotStartingActivities = GetAllActivityIdsWhichHaveInboundConnections(compositeActivityBlueprint);
var query = from activity in compositeActivityBlueprint.Activities
where !activityIdsThatAreNotStartingActivities.Contains(activity.Id)
select activity;
return query;
}
/// <summary>
/// This method gets activities that have inbound connections.
/// </summary>
/// <remarks>
/// <para>
/// "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.
/// </para>
/// </remarks>
/// <param name="compositeActivityBlueprint">A composite activity blueprint</param>
/// <returns>A lookup of activity IDs which are not starting activities</returns>
ILookup<string?,string?> GetAllActivityIdsWhichHaveInboundConnections(ICompositeActivityBlueprint compositeActivityBlueprint)
{
return compositeActivityBlueprint.Connections
.Select(x => x.Target.Activity?.Id)
.Distinct()
.ToLookup(x => x);
}
}
}

View file

@ -14,21 +14,25 @@ namespace Elsa.Services
{
private readonly IActivityTypeService _activityTypeService;
private readonly ILogger _logger;
private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider;
public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService, ILogger<WorkflowBlueprintMaterializer> logger)
public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService,
ILogger<WorkflowBlueprintMaterializer> logger,
IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider)
{
this.startingActivitiesProvider = startingActivitiesProvider ?? throw new System.ArgumentNullException(nameof(startingActivitiesProvider));
_activityTypeService = activityTypeService;
_logger = logger;
}
public async Task<IWorkflowBlueprint> 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<ICompositeActivityBlueprint>().ToList();
var compositeActivityBlueprints = activityBlueprints.Values.Where(x => x is ICompositeActivityBlueprint).Cast<ICompositeActivityBlueprint>().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<IEnumerable<IActivityBlueprint>> CreateBlueprintsAsync(ActivityDefinition activityDefinition, CancellationToken cancellationToken)
{
var list = new List<IActivityBlueprint>();
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;

View file

@ -0,0 +1,34 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// Default implementation of <see cref="ICreatesWorkflowExecutionContextForWorkflowBlueprint"/>.
/// </summary>
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));
}
/// <summary>
/// Creates a workflow execution context for the specified workflow blueprint.
/// </summary>
/// <param name="workflowBlueprint">A workflow blueprint</param>
/// <param name="cancellationToken">An optional cancellation token</param>
/// <returns>A task for a workflow execution context</returns>
public async Task<WorkflowExecutionContext> CreateWorkflowExecutionContextAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default)
{
var workflowInstance = await workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken);
return new WorkflowExecutionContext(serviceProvider, workflowBlueprint, workflowInstance);
}
}
}

View file

@ -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<WorkflowInstance> 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");

View file

@ -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<IWorkflowBuilder> workflowBuilderFactory,
IMediator mediator,
IServiceScopeFactory serviceScopeFactory,
ILogger<WorkflowRunner> logger)
ILogger<WorkflowRunner> 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<bool> 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;

View file

@ -17,36 +17,24 @@ namespace Elsa.Triggers
public class TriggerIndexer : ITriggerIndexer
{
private readonly IWorkflowRegistry _workflowRegistry;
private readonly IBookmarkHasher _bookmarkHasher;
private readonly IEnumerable<IBookmarkProvider> _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<IBookmarkProvider> providers,
IServiceProvider serviceProvider,
IWorkflowFactory workflowFactory,
IActivityTypeService activityTypeService,
ITriggerStore triggerStore,
IMediator mediator,
ILogger<TriggerIndexer> logger)
ILogger<TriggerIndexer> 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<IEnumerable<WorkflowTrigger>> GetTriggersAsync(ICollection<IWorkflowBlueprint> workflowBlueprints, CancellationToken cancellationToken)
{
var allTriggers = new List<WorkflowTrigger>();
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<IBookmarkProvider> FilterProvidersAsync(BookmarkProviderContext context)
{
foreach (var provider in _providers)
if (await provider.SupportsActivityAsync(context))
yield return provider;
}
}
}

View file

@ -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
{
/// <summary>
/// Default implementation of <see cref="IGetsTriggersForActivityBlueprintAndWorkflow"/>.
/// </summary>
public class TriggersForActivityBlueprintAndWorkflowProvider : IGetsTriggersForActivityBlueprintAndWorkflow
{
readonly IBookmarkHasher bookmarkHasher;
readonly IEnumerable<IBookmarkProvider> bookmarkProviders;
readonly ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory;
public TriggersForActivityBlueprintAndWorkflowProvider(IBookmarkHasher bookmarkHasher,
IEnumerable<IBookmarkProvider> 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));
}
/// <summary>
/// Gets a collection of the workflow triggers for the specified activity blueprint.
/// </summary>
/// <param name="activityBlueprint">An activity blueprint</param>
/// <param name="workflowExecutionContext">A workflow execution context</param>
/// <param name="activityTypes">A dictionary of all of the activity types (by name)</param>
/// <param name="cancellationToken">An optional cancellation token</param>
/// <returns>A task exposing a collection of workflow triggers for the activity and workflow.</returns>
public async Task<IEnumerable<WorkflowTrigger>> GetTriggersForActivityBlueprintAsync(IActivityBlueprint activityBlueprint,
WorkflowExecutionContext workflowExecutionContext,
IDictionary<string, ActivityType> 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<string,ActivityType> activityTypes)
{
var activityExecutionContext = activityExecutionContextFactory.CreateActivityExecutionContext(activity,
workflowExecutionContext,
cancellationToken);
var activityType = activityTypes[activity.Type];
return new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint);
}
async IAsyncEnumerable<IBookmarkProvider> GetSupportedBookmarkProvidersForContextAsync(BookmarkProviderContext context)
{
foreach (var provider in bookmarkProviders)
if (await provider.SupportsActivityAsync(context))
yield return provider;
}
async Task<IList<WorkflowTrigger>> 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();
}
}
}

View file

@ -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
{
/// <summary>
/// Default implementation of <see cref="IGetsTriggersForWorkflowBlueprints"/> which
/// gets all of the workflow triggers for a collection of workflow blueprints.
/// </summary>
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));
}
/// <summary>
/// Gets the triggers for all of the specified workflow blueprints.
/// </summary>
/// <param name="workflowBlueprints">The workflow blueprints for which to get triggers.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>A task which exposes an enumerable collection of workflow triggers.</returns>
public async Task<IEnumerable<WorkflowTrigger>> GetTriggersAsync(IEnumerable<IWorkflowBlueprint> 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<IList<WorkflowTrigger>> GetWorkflowTriggersForWorkflowBlueprintAsync(IWorkflowBlueprint workflowBlueprint,
IDictionary<string, ActivityType> 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();
}
}
}

View file

@ -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<DuplicateActivitiesWorkflow>()
.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);

View file

@ -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();
});

View file

@ -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<IEnumerable<WorkflowTrigger>> IndexThenGetAllTriggersAsync()
{
var serviceProvider = await GetServiceProvider();
var sut = serviceProvider.GetRequiredService<ITriggerIndexer>();
await sut.IndexTriggersAsync();
var triggerStore = serviceProvider.GetRequiredService<ITriggerStore>();
return await triggerStore.GetAsync();
}
async Task<IServiceProvider> GetServiceProvider()
{
var services = new ServiceCollection();
services.AddElsa(elsa => {
elsa
.AddWorkflow<WorkflowWithBlockingStartActivity>()
.AddWorkflow<WorkflowWithNonBlockingStartActivity>()
.AddActivity<SignalReceived>()
.AddActivity<SetVariable>()
;
});
var serviceProvider = services.BuildServiceProvider();
var definitionStore = serviceProvider.GetRequiredService<IWorkflowDefinitionStore>();
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<SignalReceived>(a => a.Set(x => x.Signal, "MySignal").Set(x => x.Id, "SignalReceived1"))
.Then<Finish>(a => a.Set(x => x.Id, "Finish1"));
}
}
class WorkflowWithNonBlockingStartActivity : IWorkflow
{
public void Build(IWorkflowBuilder builder)
{
builder
.StartWith<SetVariable>(t => t.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused").Set(x => x.Id, "SetVariable1"))
.Then<Finish>(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),
}
};
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<IEnumerable<IBookmarkProvider>>())
return new NoSpecimen();
return Enumerable.Range(0, howMany)
.Select(x => Mock.Of<IBookmarkProvider>())
.ToList();
}
public MockBookmarkProvidersSpecimenBuilder(int howMany)
{
this.howMany = howMany;
}
}
}

View file

@ -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");
}
}
}

View file

@ -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<string,IActivityBlueprint> idsToBlueprints)
{
foreach(var kvp in idsToBlueprints)
Mock.Get(kvp.Value).SetupGet(x => x.Id).Returns(kvp.Key);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<BookmarkProviderContext>(c => c.ActivityType == activityType), default))
.Returns(() => ValueTask.FromResult(true));
Mock.Get(unsupportedBookmarkProvider)
.Setup(x => x.SupportsActivityAsync(It.Is<BookmarkProviderContext>(c => c.ActivityType == activityType), default))
.Returns(() => ValueTask.FromResult(false));
Mock.Get(bookmarkProvider2)
.Setup(x => x.SupportsActivityAsync(It.Is<BookmarkProviderContext>(c => c.ActivityType == activityType), default))
.Returns(() => ValueTask.FromResult(true));
Mock.Get(bookmarkProvider1)
.Setup(x => x.GetBookmarksAsync(It.IsAny<BookmarkProviderContext>(), default))
.Returns(() => ValueTask.FromResult<IEnumerable<IBookmark>>(new [] { bookmark1, bookmark2 }));
Mock.Get(bookmarkProvider2)
.Setup(x => x.GetBookmarksAsync(It.IsAny<BookmarkProviderContext>(), default))
.Returns(() => ValueTask.FromResult<IEnumerable<IBookmark>>(new [] { bookmark3, bookmark4 }));
var result = await sut.GetTriggersForActivityBlueprintAsync(activityBlueprint,
workflowExecutionContext,
new Dictionary<string, ActivityType> { { 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");
}
}
}

View file

@ -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<IEnumerable<ActivityType>>(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<IWorkflowBlueprint>(), default))
.Returns((IWorkflowBlueprint bp, CancellationToken c) => Task.FromResult(new WorkflowExecutionContext(serviceProvider, bp, workflowInstance, default)));
Mock.Get(triggerProvider)
.Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint1, It.IsAny<WorkflowExecutionContext>(), It.IsAny<IDictionary<string,ActivityType>>(), default))
.Returns(() => Task.FromResult<IEnumerable<WorkflowTrigger>>(new [] { trigger1, trigger2 }));
Mock.Get(triggerProvider)
.Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint2, It.IsAny<WorkflowExecutionContext>(), It.IsAny<IDictionary<string,ActivityType>>(), default))
.Returns(() => Task.FromResult<IEnumerable<WorkflowTrigger>>(new [] { trigger3, trigger4 }));
Mock.Get(triggerProvider)
.Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint3, It.IsAny<WorkflowExecutionContext>(), It.IsAny<IDictionary<string,ActivityType>>(), default))
.Returns(() => Task.FromResult<IEnumerable<WorkflowTrigger>>(new [] { trigger5, trigger6 }));
var results = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 });
Assert.Equal(new [] { trigger1, trigger2, trigger3, trigger4, trigger5, trigger6 },
results);
}
}
}