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.