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.
This commit is contained in:
Sipke Schoorstra 2024-03-21 08:22:11 +01:00 committed by GitHub
parent e67a6fe752
commit d11754324e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 145 additions and 59 deletions

View file

@ -0,0 +1,14 @@
using Elsa.Alterations.Core.Models;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Represents a service that can find workflow instances based on specified filters.
/// </summary>
public interface IWorkflowInstanceFinder
{
/// <summary>
/// Finds workflow instances based on the specified filter.
/// </summary>
Task<IEnumerable<string>> FindAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
}

View file

@ -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<AlterationOptions>(_ => { }); // Ensure that the options are configured even if the application doesn't do so.
services.AddScoped<IAlteredWorkflowDispatcher, DefaultAlteredWorkflowDispatcher>();
services.AddScoped<IAlteredWorkflowDispatcher, AlteredWorkflowDispatcher>();
services.AddScoped<IWorkflowInstanceFinder, WorkflowInstanceFinder>();
services.AddSingleton<IAlterationSerializer, AlterationSerializer>();
services.AddSerializationOptionsConfigurator<AlterationSerializationOptionConfigurator>();
return services;

View file

@ -12,7 +12,12 @@ public class ActivityFilter
/// <summary>
/// The ID of the activity.
/// </summary>
public string? Id { get; set; }
public string? ActivityId { get; set; }
/// <summary>
/// The ID of the activity instance.
/// </summary>
public string? ActivityInstanceId { get; set; }
/// <summary>
/// The node ID of the activity.

View file

@ -33,6 +33,11 @@ public class AlterationWorkflowInstanceFilter
/// Whether the workflow instances to match have incidents.
/// </summary>
public bool? HasIncidents { get; set; }
/// <summary>
/// Whether the workflow instances to match are system workflows. Defaults to <c>false</c>.
/// </summary>
public bool? IsSystem { get; set; } = false;
/// <summary>
/// Represents a collection of filters for activities.

View file

@ -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;
/// <inheritdoc />
public class DefaultAlteredWorkflowDispatcher : IAlteredWorkflowDispatcher
public class AlteredWorkflowDispatcher : IAlteredWorkflowDispatcher
{
private readonly IWorkflowDispatcher _workflowDispatcher;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAlteredWorkflowDispatcher"/> class.
/// Initializes a new instance of the <see cref="AlteredWorkflowDispatcher"/> class.
/// </summary>
public DefaultAlteredWorkflowDispatcher(IWorkflowDispatcher workflowDispatcher)
public AlteredWorkflowDispatcher(IWorkflowDispatcher workflowDispatcher)
{
_workflowDispatcher = workflowDispatcher;
}

View file

@ -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;
/// <inheritdoc />
public class WorkflowInstanceFinder(IWorkflowInstanceStore workflowInstanceStore, IActivityExecutionStore activityExecutionStore) : IWorkflowInstanceFinder
{
/// <inheritdoc />
public async Task<IEnumerable<string>> 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<string>().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);
}
}

View file

@ -84,37 +84,8 @@ public class GenerateAlterationJobs : CodeActivity<int>
private async Task<IEnumerable<string>> 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<IWorkflowInstanceStore>();
var activityExecutionStore = context.GetRequiredService<IActivityExecutionStore>();
var workflowInstanceIds = workflowInstanceFilter.IsEmpty ? Enumerable.Empty<string>().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<IWorkflowInstanceFinder>();
return await workflowInstanceFinder.FindAsync(filter, cancellationToken);
}
private async Task GenerateJobsAsync(ActivityExecutionContext context, AlterationPlan plan, IEnumerable<string> workflowInstanceIds)

View file

@ -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;
/// <summary>
/// Executes an alteration plan.
/// </summary>
[PublicAPI]
public class DryRun(IWorkflowInstanceFinder workflowInstanceFinder) : ElsaEndpoint<AlterationWorkflowInstanceFilter, Response>
{
/// <inheritdoc />
public override void Configure()
{
Post("/alterations/dry-run");
ConfigurePermissions("run:alterations");
}
/// <inheritdoc />
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);
}
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Alterations.Endpoints.Alterations.DryRun;
/// <summary>
/// The response from the <see cref="DryRun"/> endpoint.
/// </summary>
public record Response(ICollection<string> WorkflowInstanceIds);

View file

@ -95,28 +95,6 @@ public class WorkflowInstanceFilter
/// Filter workflow instances by timestamp.
/// </summary>
public ICollection<TimestampFilter>? TimestampFilters { get; set; }
/// <summary>
/// Returns true if the filter is empty.
/// </summary>
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);
/// <summary>
/// Applies the filter to the specified query.