Implement Activity Testing API (#6719)
* Adds activity test-run endpoint Adds an API endpoint to facilitate testing of individual activities within a workflow. The endpoint allows developers to execute a specific activity within a given workflow definition. Also ensures latest workflow definition is marked when publishing. * Enables activity test run to return results Updates the activity test run endpoint to return activity execution results such as outputs, outcomes, exceptions, and status. This allows users to inspect the results of test runs for activities, providing valuable insights into their behavior. * Add activity testing API integration and activity test UI support Introduced `ITestsApi` and supporting models for activity testing in workflows. Updated the UI to support triggering and displaying activity test results, including handling outcomes, outputs, exceptions, and status. Additionally, updated dependency injection for the new API and adjusted the project references. * Refactor workflow test endpoints and update method parameters Updated the namespace and route for activity test endpoints from `TestRun` to `Tests/Activities`. Adjusted default parameter values in `WorkflowDefinitionHandle` method for improved consistency. * Introduce `ActivityTestRunner` to streamline activity testing Adds a new `IActivityTestRunner` interface and its implementation, simplifying the execution of individual workflow activities for testing purposes. Updates dependencies and refactors activity test endpoints to use the new service. * Update `ActivityTestRunner` to support variable test values Enhanced the `ActivityTestRunner` to include functionality for injecting test-specific variable values when executing workflow activities. Added a helper method `GetVariableTestValues` to retrieve and deserialize test variable data from custom properties. * Update `Tests/Activities` endpoint to include `ActivityState` Refactored the response model to replace `Outcomes` with `ActivityState` and `Payload`, ensuring more detailed activity execution results. * Add draft saving and saved notifications for workflow definitions * Add draft saving notification handling in cache evictions Integrates handling for `WorkflowDefinitionDraftSaving` and `WorkflowDefinitionDraftSaved` events within the workflow cache eviction service, ensuring definitions are properly evicted. * Refactor `ConfigureLiquidEngine` to simplify variable enumeration logic * Remove redundant `<returns>` tag from `ByDefinitionVersionId` method XML documentation * Refactor `Tests/Activities` endpoint documentation and constructor Updated XML documentation to clarify endpoint responsibilities and adjusted constructor by removing an unused dependency (`IIdentityGenerator`).
This commit is contained in:
parent
2bf9ecc709
commit
e8cd3cded5
|
|
@ -12,6 +12,7 @@ using Elsa.Api.Client.Resources.Resilience.Contracts;
|
|||
using Elsa.Api.Client.Resources.Scripting.Contracts;
|
||||
using Elsa.Api.Client.Resources.StorageDrivers.Contracts;
|
||||
using Elsa.Api.Client.Resources.Tasks.Contracts;
|
||||
using Elsa.Api.Client.Resources.Tests;
|
||||
using Elsa.Api.Client.Resources.VariableTypes.Contracts;
|
||||
using Elsa.Api.Client.Resources.WorkflowActivationStrategies.Contracts;
|
||||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
|
||||
|
|
@ -86,6 +87,7 @@ public static class DependencyInjectionExtensions
|
|||
services.AddApi<IWorkflowContextProviderDescriptorsApi>(builderOptions);
|
||||
services.AddApi<IAlterationsApi>(builderOptions);
|
||||
services.AddApi<ITasksApi>(builderOptions);
|
||||
services.AddApi<ITestsApi>(builderOptions);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
18
src/clients/Elsa.Api.Client/Resources/Tests/ITestsApi.cs
Normal file
18
src/clients/Elsa.Api.Client/Resources/Tests/ITestsApi.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Refit;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a client for the testing API.
|
||||
/// </summary>
|
||||
public interface ITestsApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends the specified request to the login API.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The response.</returns>
|
||||
[Post("/tests/activities")]
|
||||
Task<TestActivityResponse> TestActivityAsync([Body] TestActivityRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.Tests;
|
||||
|
||||
public class TestActivityRequest
|
||||
{
|
||||
public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!;
|
||||
public ActivityHandle ActivityHandle { get; set; } = null!;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Api.Client.Resources.WorkflowInstances.Models;
|
||||
using Elsa.Api.Client.Shared.Models;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.Tests;
|
||||
|
||||
public class TestActivityResponse
|
||||
{
|
||||
public IDictionary<string, object?>? Outputs { get; set; }
|
||||
public ICollection<string>? Outcomes { get; set; }
|
||||
public ExceptionState? Exception { get; set; }
|
||||
public ActivityStatus Status { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a handle to an activity.
|
||||
/// </summary>
|
||||
public class ActivityHandle
|
||||
{
|
||||
public static ActivityHandle FromActivityId(string activityId) => new() { ActivityId = activityId };
|
||||
public static ActivityHandle FromActivityNodeId(string activityNodeId) => new() { ActivityNodeId = activityNodeId };
|
||||
public static ActivityHandle FromActivityInstanceId(string activityInstanceId) => new() { ActivityInstanceId = activityInstanceId };
|
||||
public static ActivityHandle FromActivityHash(string activityHash) => new() { ActivityHash = activityHash };
|
||||
public string? ActivityId { get; init; }
|
||||
public string? ActivityNodeId { get; init;}
|
||||
public string? ActivityInstanceId { get; init;}
|
||||
public string? ActivityHash { get; init;}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using Elsa.Api.Client.Shared.Models;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a handle to a workflow definition.
|
||||
/// </summary>
|
||||
public class WorkflowDefinitionHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the definition ID. When set, the <see cref="DefinitionVersionId"/> property is ignored.
|
||||
/// </summary>
|
||||
public string? DefinitionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the version options. When set, the <see cref="DefinitionVersionId"/> property is ignored.
|
||||
/// </summary>
|
||||
public VersionOptions? VersionOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the definition version ID. When set, the <see cref="DefinitionId"/> and <see cref="VersionOptions"/> properties are ignored.
|
||||
/// </summary>
|
||||
public string? DefinitionVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkflowDefinitionHandle"/> instance with the specified definition ID and version options.
|
||||
/// </summary>
|
||||
public static WorkflowDefinitionHandle ByDefinitionId(string definitionId, VersionOptions? versionOptions = null) => new() { DefinitionId = definitionId, VersionOptions = versionOptions };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkflowDefinitionHandle"/> instance with the specified definition version ID.
|
||||
/// </summary>
|
||||
/// <param name="definitionVersionId"></param>
|
||||
public static WorkflowDefinitionHandle ByDefinitionVersionId(string definitionVersionId) => new() { DefinitionVersionId = definitionVersionId };
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (DefinitionId != null)
|
||||
return $"DefinitionId: {DefinitionId}, VersionOptions: {VersionOptions}";
|
||||
|
||||
if (DefinitionVersionId != null)
|
||||
return $"DefinitionVersionId: {DefinitionVersionId}";
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -91,29 +91,11 @@ internal class ConfigureLiquidEngine : INotificationHandler<RenderingLiquidTempl
|
|||
|
||||
private static object? GetVariableInScope(ExpressionExecutionContext context, string variableName)
|
||||
{
|
||||
var q = from variable in EnumerateVariablesInScope(context)
|
||||
var q = from variable in context.EnumerateVariablesInScope()
|
||||
where variable.Name == variableName
|
||||
where variable.TryGet(context, out _)
|
||||
select variable.Get(context);
|
||||
|
||||
return q.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<Variable> EnumerateVariablesInScope(ExpressionExecutionContext context)
|
||||
{
|
||||
var currentScope = context;
|
||||
|
||||
while (currentScope != null)
|
||||
{
|
||||
if (!currentScope.TryGetActivityExecutionContext(out var activityExecutionContext))
|
||||
break;
|
||||
|
||||
var variables = activityExecutionContext.Variables;
|
||||
|
||||
foreach (var variable in variables)
|
||||
yield return variable;
|
||||
|
||||
currentScope = currentScope.ParentContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.State;
|
||||
|
||||
namespace Elsa.Workflows.Api.Endpoints.Tests.Activities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an endpoint for testing activities in workflows. This endpoint is responsible for handling requests
|
||||
/// that test the execution of a specific activity in a workflow and returning the results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This endpoint is used to perform operations such as:
|
||||
/// - Finding a workflow graph based on a provided workflow definition handle.
|
||||
/// - Locating and executing a specific activity within the workflow graph.
|
||||
/// - Capturing the execution results and returning them as a response.
|
||||
/// </remarks>
|
||||
internal class Endpoint(
|
||||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
IActivityTestRunner activityTestRunner,
|
||||
IActivityExecutionMapper activityExecutionMapper)
|
||||
: ElsaEndpoint<Request>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/tests/activities");
|
||||
ConfigurePermissions("exec:tests");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
|
||||
{
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(request.WorkflowDefinitionHandle, cancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
{
|
||||
AddError("Workflow definition not found.");
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var activity = workflowGraph.FindActivity(request.ActivityHandle);
|
||||
|
||||
if (activity == null)
|
||||
{
|
||||
AddError("Activity not found.");
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var activityExecutionContext = await activityTestRunner.RunAsync(workflowGraph, activity, cancellationToken);
|
||||
var record = activityExecutionMapper.Map(activityExecutionContext);
|
||||
var activityState = record.ActivityState ?? new Dictionary<string, object?>();
|
||||
|
||||
var response = new Response
|
||||
{
|
||||
ActivityState = activityState,
|
||||
Outputs = record.Outputs,
|
||||
Payload = record.Payload,
|
||||
Exception = record.Exception,
|
||||
Status = record.Status
|
||||
};
|
||||
|
||||
await SendOkAsync(response, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class Request
|
||||
{
|
||||
public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!;
|
||||
public ActivityHandle ActivityHandle { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class Response
|
||||
{
|
||||
public IDictionary<string, object?> ActivityState { get; set; } = null!;
|
||||
public IDictionary<string, object?>? Outputs { get; set; }
|
||||
public IDictionary<string, object>? Payload { get; set; }
|
||||
public ExceptionState? Exception { get; set; }
|
||||
public ActivityStatus Status { get; set; }
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// <summary>
|
||||
/// Returns the global node ID for the current activity within the graph.
|
||||
/// </summary>
|
||||
/// <remarks>As of tool version 3.0, all activity Ids are already unique, so there's no need to construct a hierarchical ID</remarks>
|
||||
/// <remarks>As of tool version 3.0, all activity IDs are already unique, so there's no need to construct a hierarchical ID</remarks>
|
||||
public string NodeId => ActivityNode.NodeId;
|
||||
|
||||
public ISet<ActivityExecutionContext> Children { get; } = new HashSet<ActivityExecutionContext>();
|
||||
|
|
|
|||
|
|
@ -85,6 +85,28 @@ public partial class WorkflowExecutionContext : IExecutionContext
|
|||
_cancellationTokenSources.Add(linkedCancellationTokenSource);
|
||||
_cancellationRegistrations.Add(linkedCancellationTokenSource.Token.Register(CancelWorkflow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkflowExecutionContext"/> for the specified workflow.
|
||||
/// </summary>
|
||||
public static async Task<WorkflowExecutionContext> CreateAsync(
|
||||
IServiceProvider serviceProvider,
|
||||
WorkflowGraph workflowGraph,
|
||||
string id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var systemClock = serviceProvider.GetRequiredService<ISystemClock>();
|
||||
|
||||
return await CreateAsync(
|
||||
serviceProvider,
|
||||
workflowGraph,
|
||||
id,
|
||||
new List<ActivityIncident>(),
|
||||
new List<Bookmark>(),
|
||||
systemClock.UtcNow,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkflowExecutionContext"/> for the specified workflow.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public interface IActivityInvoker
|
|||
/// <param name="workflowExecutionContext">The workflow execution context.</param>
|
||||
/// <param name="activity">The activity to invoke.</param>
|
||||
/// <param name="options"></param>
|
||||
Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = default);
|
||||
Task<ActivityExecutionContext> InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified activity execution context.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
public interface IActivityTestRunner
|
||||
{
|
||||
Task<ActivityExecutionContext> RunAsync(WorkflowGraph workflowGraph, IActivity activity, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -10,14 +10,14 @@ namespace Elsa.Workflows;
|
|||
/// </summary>
|
||||
public interface IWorkflowRunner
|
||||
{
|
||||
Task<RunWorkflowResult> RunAsync(IActivity activity, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(IWorkflow workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult<TResult>> RunAsync<TResult>(WorkflowBase<TResult> workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync<T>(RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) where T : IWorkflow, new();
|
||||
Task<TResult> RunAsync<T, TResult>(RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) where T : WorkflowBase<TResult>, new();
|
||||
Task<RunWorkflowResult> RunAsync(Workflow workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(WorkflowGraph workflowGraph, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(WorkflowGraph workflowGraph, WorkflowState workflowState, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(IActivity activity, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(IWorkflow workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult<TResult>> RunAsync<TResult>(WorkflowBase<TResult> workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync<T>(RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) where T : IWorkflow, new();
|
||||
Task<TResult> RunAsync<T, TResult>(RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) where T : WorkflowBase<TResult>, new();
|
||||
Task<RunWorkflowResult> RunAsync(Workflow workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(WorkflowGraph workflowGraph, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(WorkflowGraph workflowGraph, WorkflowState workflowState, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<RunWorkflowResult> RunAsync(WorkflowExecutionContext workflowExecutionContext);
|
||||
}
|
||||
|
|
@ -163,6 +163,7 @@ public class WorkflowsFeature : FeatureBase
|
|||
// Core.
|
||||
.AddScoped<IActivityInvoker, ActivityInvoker>()
|
||||
.AddScoped<IWorkflowRunner, WorkflowRunner>()
|
||||
.AddScoped<IActivityTestRunner, ActivityTestRunner>()
|
||||
.AddScoped<IActivityVisitor, ActivityVisitor>()
|
||||
.AddScoped<IIdentityGraphService, IdentityGraphService>()
|
||||
.AddScoped<IWorkflowGraphBuilder, WorkflowGraphBuilder>()
|
||||
|
|
|
|||
|
|
@ -82,14 +82,6 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I
|
|||
// Invoke next middleware.
|
||||
await next(context);
|
||||
|
||||
// // If the activity created any bookmarks, copy them into the workflow execution context.
|
||||
// if (context.Bookmarks.Any())
|
||||
// {
|
||||
// // Store bookmarks.
|
||||
// workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks);
|
||||
// logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count);
|
||||
// }
|
||||
|
||||
// Conditionally commit the workflow state.
|
||||
if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuted))
|
||||
await context.WorkflowExecutionContext.CommitAsync();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class WorkflowDefinitionHandle
|
|||
/// <summary>
|
||||
/// Creates a new <see cref="WorkflowDefinitionHandle"/> instance with the specified definition ID and version options.
|
||||
/// </summary>
|
||||
public static WorkflowDefinitionHandle ByDefinitionId(string definitionId, VersionOptions? versionOptions = default) => new() { DefinitionId = definitionId, VersionOptions = versionOptions };
|
||||
public static WorkflowDefinitionHandle ByDefinitionId(string definitionId, VersionOptions? versionOptions = null) => new() { DefinitionId = definitionId, VersionOptions = versionOptions };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkflowDefinitionHandle"/> instance with the specified definition version ID.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,54 @@ public record WorkflowGraph
|
|||
/// </summary>
|
||||
public IDictionary<string, ActivityNode> NodeIdLookup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Finds the activity based on the provided <paramref name="handle"/>.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle containing the identification parameters for the activity.</param>
|
||||
/// <returns>The activity found based on the handle, or null if no activity is found.</returns>
|
||||
public IActivity? FindActivity(ActivityHandle handle)
|
||||
{
|
||||
return handle.ActivityId != null
|
||||
? FindActivityById(handle.ActivityId)
|
||||
: handle.ActivityNodeId != null
|
||||
? FindActivityByNodeId(handle.ActivityNodeId)
|
||||
: handle.ActivityHash != null
|
||||
? FindActivityByHash(handle.ActivityHash)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="ActivityNode"/> with the specified activity ID from the workflow graph.
|
||||
/// </summary>
|
||||
public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="ActivityNode"/> with the specified hash of the activity node ID from the workflow graph.
|
||||
/// </summary>
|
||||
/// <param name="hash">The hash of the activity node ID.</param>
|
||||
/// <returns>The <see cref="ActivityNode"/> with the specified hash of the activity node ID.</returns>
|
||||
public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : null;
|
||||
|
||||
/// Returns the <see cref="ActivityNode"/> containing the specified activity from the workflow graph.
|
||||
public ActivityNode? FindNodeByActivity(IActivity activity)
|
||||
{
|
||||
return NodeActivityLookup.TryGetValue(activity, out var node) ? node : null;
|
||||
}
|
||||
|
||||
/// Returns the <see cref="ActivityNode"/> associated with the specified activity ID.
|
||||
public ActivityNode? FindNodeByActivityId(string activityId) => Nodes.FirstOrDefault(x => x.Activity.Id == activityId);
|
||||
|
||||
/// Returns the <see cref="IActivity"/> with the specified ID from the workflow graph.
|
||||
public IActivity? FindActivityByNodeId(string nodeId) => FindNodeById(nodeId)?.Activity;
|
||||
|
||||
/// Returns the <see cref="IActivity"/> with the specified ID from the workflow graph.
|
||||
public IActivity? FindActivityById(string activityId) => FindNodeById(NodeIdLookup.SingleOrDefault(n => n.Key.EndsWith(activityId)).Value.NodeId)?.Activity;
|
||||
|
||||
/// Returns the <see cref="IActivity"/> with the specified hash of the activity node ID from the workflow graph.
|
||||
/// <param name="hash">The hash of the activity node ID.</param>
|
||||
/// <returns>The <see cref="IActivity"/> with the specified hash of the activity node ID.</returns>
|
||||
public IActivity? FindActivityByHash(string hash) => FindNodeByHash(hash)?.Activity;
|
||||
|
||||
private static string Hash(HashAlgorithm hashAlgorithm, string input)
|
||||
{
|
||||
var data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public class ActivityInvoker(
|
|||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null)
|
||||
public async Task<ActivityExecutionContext> InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null)
|
||||
{
|
||||
// Setup an activity execution context, potentially reusing an existing one if requested.
|
||||
var existingActivityExecutionContext = options?.ExistingActivityExecutionContext;
|
||||
|
|
@ -34,6 +34,8 @@ public class ActivityInvoker(
|
|||
|
||||
// Execute the activity execution pipeline.
|
||||
await InvokeAsync(activityExecutionContext);
|
||||
|
||||
return activityExecutionContext;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ActivityTestRunner(
|
||||
IServiceProvider serviceProvider,
|
||||
IWorkflowExecutionPipeline pipeline,
|
||||
IIdentityGenerator identityGenerator)
|
||||
: IActivityTestRunner
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ActivityExecutionContext> RunAsync(WorkflowGraph workflowGraph, IActivity activity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var id = identityGenerator.GenerateId();
|
||||
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(serviceProvider, workflowGraph, id, cancellationToken);
|
||||
var variableTestValues = GetVariableTestValues(workflowGraph);
|
||||
|
||||
foreach (var variable in workflowGraph.Workflow.Variables)
|
||||
{
|
||||
var variableValue = variableTestValues.TryGetValue(variable.Id, out var value) ? value : variable.Value;
|
||||
variable.Set(workflowExecutionContext.ExpressionExecutionContext!, variableValue);
|
||||
}
|
||||
|
||||
workflowExecutionContext.ScheduleActivity(activity);
|
||||
workflowExecutionContext.TransitionTo(WorkflowSubStatus.Executing);
|
||||
|
||||
await pipeline.ExecuteAsync(workflowExecutionContext);
|
||||
var activityExecutionContext = workflowExecutionContext
|
||||
.ActivityExecutionContexts
|
||||
.First(x => x.Activity == activity);
|
||||
return activityExecutionContext;
|
||||
}
|
||||
|
||||
private IDictionary<string, object?> GetVariableTestValues(WorkflowGraph workflowGraph)
|
||||
{
|
||||
var variableTestValues = workflowGraph.Workflow.CustomProperties.TryGetValue("VariableTestValues", out var variableTestValuesObj) ? variableTestValuesObj : null;
|
||||
|
||||
if (variableTestValues is JsonElement jsonElement)
|
||||
variableTestValues = JsonSerializer.Deserialize<Dictionary<string, object?>>(jsonElement.GetRawText());
|
||||
|
||||
return variableTestValues as IDictionary<string, object?> ?? new Dictionary<string, object?>();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,28 +12,34 @@ namespace Elsa.Workflows.Management.Handlers.Notifications;
|
|||
/// </remarks>
|
||||
[UsedImplicitly]
|
||||
internal class EvictWorkflowDefinitionServiceCache(IWorkflowDefinitionCacheManager workflowDefinitionCacheManager) :
|
||||
INotificationHandler<WorkflowDefinitionDraftSaving>,
|
||||
INotificationHandler<WorkflowDefinitionPublishing>,
|
||||
INotificationHandler<WorkflowDefinitionRetracting>,
|
||||
INotificationHandler<WorkflowDefinitionDeleting>,
|
||||
INotificationHandler<WorkflowDefinitionsDeleting>,
|
||||
INotificationHandler<WorkflowDefinitionVersionsUpdating>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(WorkflowDefinitionPublishing notification, CancellationToken cancellationToken)
|
||||
public Task HandleAsync(WorkflowDefinitionDraftSaving notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task HandleAsync(WorkflowDefinitionPublishing notification, CancellationToken cancellationToken)
|
||||
{
|
||||
return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(WorkflowDefinitionRetracting notification, CancellationToken cancellationToken)
|
||||
public Task HandleAsync(WorkflowDefinitionRetracting notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(WorkflowDefinitionDeleting notification, CancellationToken cancellationToken)
|
||||
public Task HandleAsync(WorkflowDefinitionDeleting notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.DefinitionId, cancellationToken);
|
||||
return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.DefinitionId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Workflows.Management.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// A notification that is sent when a workflow definition has been updated.
|
||||
/// </summary>
|
||||
/// <param name="WorkflowDefinition">The workflow definition.</param>
|
||||
[PublicAPI]
|
||||
public record WorkflowDefinitionDraftSaved(WorkflowDefinition WorkflowDefinition) : INotification;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Workflows.Management.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// A notification that is sent when a workflow definition is being updated.
|
||||
/// </summary>
|
||||
/// <param name="WorkflowDefinition">The workflow definition.</param>
|
||||
[PublicAPI]
|
||||
public record WorkflowDefinitionDraftSaving(WorkflowDefinition WorkflowDefinition) : INotification;
|
||||
|
|
@ -113,6 +113,7 @@ public class WorkflowDefinitionPublisher(
|
|||
|
||||
// Save the newly published definition.
|
||||
definition.IsPublished = true;
|
||||
definition.IsLatest = true;
|
||||
definition = Initialize(definition);
|
||||
await workflowDefinitionStore.SaveAsync(definition, cancellationToken);
|
||||
|
||||
|
|
@ -194,7 +195,9 @@ public class WorkflowDefinitionPublisher(
|
|||
draft.IsLatest = true;
|
||||
draft = Initialize(draft);
|
||||
|
||||
await mediator.SendAsync(new WorkflowDefinitionDraftSaving(draft), cancellationToken);
|
||||
await workflowDefinitionStore.SaveAsync(draft, cancellationToken);
|
||||
await mediator.SendAsync(new WorkflowDefinitionDraftSaved(draft), cancellationToken);
|
||||
|
||||
if (lastVersion is null)
|
||||
await mediator.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken);
|
||||
|
|
|
|||
Loading…
Reference in a new issue