From d11754324e3d86f2eeb81682ce2c391fd5853f75 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 21 Mar 2024 08:22:11 +0100 Subject: [PATCH] Add DryRun endpoint and workflow instance finder (#5110) * Add DryRun endpoint and workflow instance finder Added a new DryRun endpoint for executing alteration plans and implemented the IWorkflowInstanceFinder interface to search for workflow instances. Refactored existing code to use new workflow instance finder service, resulting in cleaner activity implementation. * Fix empty check in WorkflowInstanceFinder Change the conditional logic to use `IsEmpty` method on filters rather than checking if the collection `workflowInstanceIds` is empty. This ensures that matching workflow instances are correctly identified when no specific filters are set. * Refactor WorkflowInstanceFinder with WorkflowStatus default The WorkflowInstanceFinder service has been refactored to include a default workflow status of Running and to utilize a new method WorkflowFilterIsEmpty, which checks if the workflow instance filter is empty. This improves code readability and encapsulates the logic for determining an empty filter. --- .../Contracts/IWorkflowInstanceFinder.cs | 14 ++++ .../Extensions/ServiceCollectionExtensions.cs | 4 +- .../Models/ActivityFilter.cs | 7 +- .../AlterationWorkflowInstanceFilter.cs | 5 ++ .../AlteredWorkflowDispatcher.cs} | 9 ++- .../Services/WorkflowInstanceFinder.cs | 74 +++++++++++++++++++ .../Activities/GenerateAlterationJobs.cs | 33 +-------- .../Endpoints/Alterations/DryRun/Endpoint.cs | 30 ++++++++ .../Endpoints/Alterations/DryRun/Response.cs | 6 ++ .../Filters/WorkflowInstanceFilter.cs | 22 ------ 10 files changed, 145 insertions(+), 59 deletions(-) create mode 100644 src/modules/Elsa.Alterations.Core/Contracts/IWorkflowInstanceFinder.cs rename src/modules/Elsa.Alterations.Core/{Contracts/DefaultAlteredWorkflowScheduler.cs => Services/AlteredWorkflowDispatcher.cs} (74%) create mode 100644 src/modules/Elsa.Alterations.Core/Services/WorkflowInstanceFinder.cs create mode 100644 src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs create mode 100644 src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Response.cs diff --git a/src/modules/Elsa.Alterations.Core/Contracts/IWorkflowInstanceFinder.cs b/src/modules/Elsa.Alterations.Core/Contracts/IWorkflowInstanceFinder.cs new file mode 100644 index 000000000..fd325937f --- /dev/null +++ b/src/modules/Elsa.Alterations.Core/Contracts/IWorkflowInstanceFinder.cs @@ -0,0 +1,14 @@ +using Elsa.Alterations.Core.Models; + +namespace Elsa.Alterations.Core.Contracts; + +/// +/// Represents a service that can find workflow instances based on specified filters. +/// +public interface IWorkflowInstanceFinder +{ + /// + /// Finds workflow instances based on the specified filter. + /// + Task> FindAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Alterations.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.Alterations.Core/Extensions/ServiceCollectionExtensions.cs index 29b560a85..232d894fa 100644 --- a/src/modules/Elsa.Alterations.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/Elsa.Alterations.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Elsa.Alterations.Core.Contracts; using Elsa.Alterations.Core.Options; using Elsa.Alterations.Core.Serialization; +using Elsa.Alterations.Core.Services; using Elsa.Common.Contracts; using Elsa.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +20,8 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddAlterationsCore(this IServiceCollection services) { services.Configure(_ => { }); // Ensure that the options are configured even if the application doesn't do so. - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSerializationOptionsConfigurator(); return services; diff --git a/src/modules/Elsa.Alterations.Core/Models/ActivityFilter.cs b/src/modules/Elsa.Alterations.Core/Models/ActivityFilter.cs index f7e2f0ebc..f13ba2cd6 100644 --- a/src/modules/Elsa.Alterations.Core/Models/ActivityFilter.cs +++ b/src/modules/Elsa.Alterations.Core/Models/ActivityFilter.cs @@ -12,7 +12,12 @@ public class ActivityFilter /// /// The ID of the activity. /// - public string? Id { get; set; } + public string? ActivityId { get; set; } + + /// + /// The ID of the activity instance. + /// + public string? ActivityInstanceId { get; set; } /// /// The node ID of the activity. diff --git a/src/modules/Elsa.Alterations.Core/Models/AlterationWorkflowInstanceFilter.cs b/src/modules/Elsa.Alterations.Core/Models/AlterationWorkflowInstanceFilter.cs index 3730c1f87..de83e2c32 100644 --- a/src/modules/Elsa.Alterations.Core/Models/AlterationWorkflowInstanceFilter.cs +++ b/src/modules/Elsa.Alterations.Core/Models/AlterationWorkflowInstanceFilter.cs @@ -33,6 +33,11 @@ public class AlterationWorkflowInstanceFilter /// Whether the workflow instances to match have incidents. /// public bool? HasIncidents { get; set; } + + /// + /// Whether the workflow instances to match are system workflows. Defaults to false. + /// + public bool? IsSystem { get; set; } = false; /// /// Represents a collection of filters for activities. diff --git a/src/modules/Elsa.Alterations.Core/Contracts/DefaultAlteredWorkflowScheduler.cs b/src/modules/Elsa.Alterations.Core/Services/AlteredWorkflowDispatcher.cs similarity index 74% rename from src/modules/Elsa.Alterations.Core/Contracts/DefaultAlteredWorkflowScheduler.cs rename to src/modules/Elsa.Alterations.Core/Services/AlteredWorkflowDispatcher.cs index f2b6be4d0..6e988c41a 100644 --- a/src/modules/Elsa.Alterations.Core/Contracts/DefaultAlteredWorkflowScheduler.cs +++ b/src/modules/Elsa.Alterations.Core/Services/AlteredWorkflowDispatcher.cs @@ -1,18 +1,19 @@ +using Elsa.Alterations.Core.Contracts; using Elsa.Alterations.Core.Results; using Elsa.Workflows.Runtime.Contracts; using Elsa.Workflows.Runtime.Requests; -namespace Elsa.Alterations.Core.Contracts; +namespace Elsa.Alterations.Core.Services; /// -public class DefaultAlteredWorkflowDispatcher : IAlteredWorkflowDispatcher +public class AlteredWorkflowDispatcher : IAlteredWorkflowDispatcher { private readonly IWorkflowDispatcher _workflowDispatcher; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public DefaultAlteredWorkflowDispatcher(IWorkflowDispatcher workflowDispatcher) + public AlteredWorkflowDispatcher(IWorkflowDispatcher workflowDispatcher) { _workflowDispatcher = workflowDispatcher; } diff --git a/src/modules/Elsa.Alterations.Core/Services/WorkflowInstanceFinder.cs b/src/modules/Elsa.Alterations.Core/Services/WorkflowInstanceFinder.cs new file mode 100644 index 000000000..e161ac75e --- /dev/null +++ b/src/modules/Elsa.Alterations.Core/Services/WorkflowInstanceFinder.cs @@ -0,0 +1,74 @@ +using Elsa.Alterations.Core.Contracts; +using Elsa.Alterations.Core.Models; +using Elsa.Workflows; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Filters; + +namespace Elsa.Alterations.Core.Services; + +/// +public class WorkflowInstanceFinder(IWorkflowInstanceStore workflowInstanceStore, IActivityExecutionStore activityExecutionStore) : IWorkflowInstanceFinder +{ + /// + public async Task> FindAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken = default) + { + var workflowInstanceFilter = new WorkflowInstanceFilter + { + Ids = filter.WorkflowInstanceIds?.ToList(), + DefinitionVersionIds = filter.DefinitionVersionIds?.ToList(), + CorrelationIds = filter.CorrelationIds?.ToList(), + HasIncidents = filter.HasIncidents, + IsSystem = filter.IsSystem, + TimestampFilters = filter.TimestampFilters?.ToList(), + WorkflowStatus = WorkflowStatus.Running + }; + var activityExecutionFilters = filter.ActivityFilters?.Select(x => new ActivityExecutionRecordFilter + { + ActivityId = x.ActivityId, + Id = x.ActivityInstanceId, + ActivityNodeId = x.NodeId, + Name = x.Name, + Status = x.Status, + }).ToList(); + + var workflowInstanceFilterIsEmpty = WorkflowFilterIsEmpty(workflowInstanceFilter); + + var workflowInstanceIds = workflowInstanceFilterIsEmpty + ? Enumerable.Empty().ToHashSet() + : (await workflowInstanceStore.FindManyIdsAsync(workflowInstanceFilter, cancellationToken)).ToHashSet(); + + if (activityExecutionFilters == null) + return workflowInstanceIds; + + foreach (ActivityExecutionRecordFilter activityExecutionFilter in activityExecutionFilters.Where(x => !x.IsEmpty)) + { + var activityExecutionRecords = await activityExecutionStore.FindManySummariesAsync(activityExecutionFilter, cancellationToken); + var matchingWorkflowInstanceIds = activityExecutionRecords.Select(x => x.WorkflowInstanceId).ToHashSet(); + + if (workflowInstanceFilterIsEmpty) + workflowInstanceIds = matchingWorkflowInstanceIds; + else + workflowInstanceIds.IntersectWith(matchingWorkflowInstanceIds); + } + + return workflowInstanceIds; + } + + private bool WorkflowFilterIsEmpty(WorkflowInstanceFilter filter) + { + return filter.Id == null && + filter.Ids == null && + filter.DefinitionId == null && + filter.DefinitionVersionId == null && + filter.DefinitionIds == null && + filter.DefinitionVersionIds == null && + filter.Version == null && + filter.CorrelationId == null && + filter.CorrelationIds == null && + filter.HasIncidents == null && + filter.TimestampFilters == null + && string.IsNullOrWhiteSpace(filter.SearchTerm); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs b/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs index 6bf093267..b56d9354d 100644 --- a/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs +++ b/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs @@ -84,37 +84,8 @@ public class GenerateAlterationJobs : CodeActivity private async Task> FindMatchingWorkflowInstanceIdsAsync(ActivityExecutionContext context, AlterationWorkflowInstanceFilter filter) { var cancellationToken = context.CancellationToken; - var workflowInstanceFilter = new WorkflowInstanceFilter - { - Ids = filter.WorkflowInstanceIds?.ToList(), - DefinitionVersionIds = filter.DefinitionVersionIds?.ToList(), - CorrelationIds = filter.CorrelationIds?.ToList(), - HasIncidents = filter.HasIncidents, - TimestampFilters = filter.TimestampFilters?.ToList(), - }; - var activityExecutionFilters = filter.ActivityFilters?.Select(x => new ActivityExecutionRecordFilter - { - ActivityId = x.Id, - ActivityNodeId = x.NodeId, - Name = x.Name, - Status = x.Status, - }).ToList(); - - var workflowInstanceStore = context.GetRequiredService(); - var activityExecutionStore = context.GetRequiredService(); - var workflowInstanceIds = workflowInstanceFilter.IsEmpty ? Enumerable.Empty().ToHashSet() : (await workflowInstanceStore.FindManyIdsAsync(workflowInstanceFilter, cancellationToken)).ToHashSet(); - - if (activityExecutionFilters != null) - { - foreach (ActivityExecutionRecordFilter activityExecutionFilter in activityExecutionFilters.Where(x => !x.IsEmpty)) - { - var activityExecutionRecords = await activityExecutionStore.FindManySummariesAsync(activityExecutionFilter, cancellationToken); - var matchingWorkflowInstanceIds = activityExecutionRecords.Select(x => x.WorkflowInstanceId).ToHashSet(); - workflowInstanceIds.UnionWith(matchingWorkflowInstanceIds); - } - } - - return workflowInstanceIds; + var workflowInstanceFinder = context.GetRequiredService(); + return await workflowInstanceFinder.FindAsync(filter, cancellationToken); } private async Task GenerateJobsAsync(ActivityExecutionContext context, AlterationPlan plan, IEnumerable workflowInstanceIds) diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs new file mode 100644 index 000000000..eb9461dff --- /dev/null +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs @@ -0,0 +1,30 @@ +using Elsa.Abstractions; +using Elsa.Alterations.Core.Contracts; +using Elsa.Alterations.Core.Models; +using JetBrains.Annotations; + +namespace Elsa.Alterations.Endpoints.Alterations.DryRun; + +/// +/// Executes an alteration plan. +/// +[PublicAPI] +public class DryRun(IWorkflowInstanceFinder workflowInstanceFinder) : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/alterations/dry-run"); + ConfigurePermissions("run:alterations"); + } + + /// + public override async Task HandleAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken) + { + var workflowInstanceIds = await workflowInstanceFinder.FindAsync(filter, cancellationToken); + + // Write response. + var response = new Response(workflowInstanceIds.ToList()); + await SendOkAsync(response, cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Response.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Response.cs new file mode 100644 index 000000000..fdae58b05 --- /dev/null +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Response.cs @@ -0,0 +1,6 @@ +namespace Elsa.Alterations.Endpoints.Alterations.DryRun; + +/// +/// The response from the endpoint. +/// +public record Response(ICollection WorkflowInstanceIds); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs index e4e5e22bf..18fddc935 100644 --- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs +++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs @@ -95,28 +95,6 @@ public class WorkflowInstanceFilter /// Filter workflow instances by timestamp. /// public ICollection? TimestampFilters { get; set; } - - /// - /// Returns true if the filter is empty. - /// - public bool IsEmpty => - Id == null && - Ids == null && - DefinitionId == null && - DefinitionVersionId == null && - DefinitionIds == null && - DefinitionVersionIds == null && - Version == null && - CorrelationId == null && - CorrelationIds == null && - WorkflowStatus == null && - WorkflowSubStatus == null && - WorkflowStatuses == null && - WorkflowSubStatuses == null && - HasIncidents == null && - IsSystem == null && - TimestampFilters == null - && string.IsNullOrWhiteSpace(SearchTerm); /// /// Applies the filter to the specified query.