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.